Académique Documents
Professionnel Documents
Culture Documents
Chapitre 1
Éléments d’architecture des
ordinateurs
Dans ce premier chapitre, nous allons très succinctement décrire les principaux constituants matériels d’un
ordinateur (le hardware) ainsi que les principes généraux qui régissent son système d’exploitation (le software).
Ces généralités concerneront avant tout les ordinateurs personnels mais restent valables pour la plupart des
machines numériques telles les tablettes ou les smartphones.
Introduction
L’informatique au sens où on l’entend aujourd’hui est née vers la fin des années des efforts conjugués de
mathématiciens d’une part, et d’ingénieurs d’autre part.
À cette époque, certains logiciens, les plus connus étant Church et Turing, se posent la question suivante :
Qu’est-ce qu’un calcul ?, et donnent naissance à la théorie de la calculabilité, qui vise à définir précisément
ce qui est calculable de ce qui ne l’est pas. Church est sans doute le premier à avoir pensé pouvoir définir
formellement ce que l’on s’accorde à reconnaître intuitivement comme un calcul en développant un nouveau
formalisme : le « lambda-calcul », et en imposant l’idée, fondamentale en algorithmique, qu’une fonction peut
être définie par des règles de calcul.
Turing quant à lui explore une autre approche en liant la notion de calcul à la notion de machine : il imagine
une machine idéale pouvant fonctionner sans intervention humaine, qui n’avait pas (encore) de réalisation
physique mais qui semblait plausible. Est alors calculable toute fonction qui peut être exécutée par une machine
de Turing. Nous savons maintenant que ces deux approches sont équivalentes, et ceux d’entre vous qui suivront
l’option informatique en complément de ce cours pourront effleurer chacune de ces deux théories à travers
l’étude d’un langage fonctionnel, Caml, qui découle directement des concepts du lambda-calcul et en étudiant
une partie de la théorie des automates née des travaux de Turing.
En parallèle à ces travaux théoriques, des ingénieurs cherchent à construire des machines électroniques ou
électro-mécaniques capables d’exécuter des calculs complexes à grande vitesse. Parmi ces précurseurs un nom
se détache, celui de Von Neuman. Au sein du projet ENIAC ce dernier décrit dans les années un schéma
d’architecture de calculateur qui reste encore aujourd’hui d’une étonnante actualité. Rétrospectivement, on
peut affirmer que bien qu’à cette époque les mathématiciens et les ingénieurs se soient mutuellement assez
largement ignorés, ce sont les travaux de Von Neuman qui permettent la première réalisation concrète d’une
machine de Turing.
Jean-Pierre Becirspahic
1.2 informatique commune
– Enfin, la restitution de l’information utilise les périphériques de sorties que sont l’écran, l’imprimante, les
enceintes acoustiques, etc.
périphériques de sortie
programmes
unité de commande
processeur mémoire
unité de traitement
données
périphériques d’entrée
La circulation de l’information entre ces différents éléments (représentée par des flèches sur ce schéma) est
assurée par des connexions appelées bus (bus de données, bus d’adresse, bus de signal lecture/écriture. . .) ; il
s’agit tout simplement de fils conducteurs utilisés pour transmettre des signaux binaires.
Ceci reste bien évidemment schématique, et l’architecture actuelle des ordinateurs s’est enrichie depuis cette
époque, en particulier sur deux points :
– les entrées et sorties, initialement commandées par l’unité centrale, sont depuis le début des années
sous le contrôle de processeurs autonomes (c’est le cas par exemple des cartes graphiques chargées de
produire une image sur un écran) ;
– les ordinateurs comportent maintenant des processeurs multiples, qu’il s’agisse d’unité séparées ou de
« cœurs » multiples à l’intérieur d’une même puce. Cette organisation permet d’atteindre une puissance
globale de calcul élevée sans augmenter la vitesse des processeurs, limitée par les capacités d’évacuation
de la chaleur.
Néanmoins, les deux innovations majeures du modèle de Von Neuman restent d’actualité. La première innova-
tion est la séparation nette entre l’unité de commande, qui organise le flot de séquencement des instructions,
et l’unité arithmétique, chargée de l’exécution proprement dite de ces instructions. La seconde innovation,
la plus fondamentale, est l’idée d’un programme enregistré : au lieu d’être codées sur un support externe, les
instructions sont enregistrées dans la mémoire et ainsi, un programme peut être traité comme une donnée par un
autre programme. Cette idée, présente en germe dans la machine de Turing, trouvait ici sa concrétisation.
Mais avant de s’intéresser à la façon dont ces objets sont représentés en mémoire, nous allons décrire les
différentes sortes de mémoires qui coexistent au sein d’un ordinateur, et qui se distinguent par leur capacité et
leur vitesse.
registres
cache
mémoire centrale
mémoire de masse
– Les registres sont des emplacements mémoires internes au processeur dans lesquels ces derniers exécutent
les instructions (on en reparlera plus loin). Selon les processeurs leur nombre varie d’une dizaine à une
centaine, et leur capacité dépasse rarement quelques dizaines d’octets (en général 8, 16, 32 ou 64 bits par
registre).
– La mémoire cache (ou mémoire tampon) est une mémoire rapide permettant de réduire les délais d’attente
des informations stockées dans la mémoire centrale. En effet, cette dernière possède une vitesse bien moins
importante que le processeur et stocker les principales données devant être traitées par le processeur dans
le cache permet d’en accélérer le traitement. Il existe plusieurs niveaux de caches qui se distinguent par
leur vitesse d’accès, le premier niveau ayant une vitesse se rapprochant de celle des registres. La capacité
d’un cache se compte généralement en kilo-octets pour les deux premiers niveaux et en mega-octets pour
le troisième.
– La mémoire centrale contient le code et les données des programmes exécutés par le processeur ; elle se
compte actuellement en giga-octets. Elle est divisée en emplacements de taille fixe appelés bytes (en
général 8 bits, c’est-à-dire un octet) : un byte est la plus petite unité adressable d’un ordinateur. Chaque
mot-mémoire est repéré par un numéro qu’on appelle son adresse. Actuellement, les adresses sont codées
dans des registres de 32 ou 64 bits, ce qui autorise au maximum 232 ou 264 adresses différentes. Sachant
que 232 octets correspondent approximativement à 4 Go, un processeur 32 bits ne peut donc en théorie
gérer plus de 4 Go de mémoire vive. Ceci explique la généralisation actuelle des processeurs 64 bits pour
accompagner l’augmentation de la mémoire vive associée à un ordinateur.
– Enfin, la mémoire de masse désigne tous les moyens de stockage pérenne dont on dispose : disque dur, clé
USB, DVD, etc. accessibles en lecture/écriture ou en lecture seule (à proprement parler, cette mémoire de
masse s’apparente donc plus à un périphérique d’entrée / sortie). Lors du lancement d’une application,
les données essentielles sont copiées dans la mémoire centrale, et les programmes sont conçus pour
minimiser les accès à la mémoire de masse, très lente comparativement aux autres mémoires (d’ailleurs
il existe aussi un cache entre mémoire de masse et mémoire centrale pour accélérer ces transferts). La
mémoire de masse tend de plus en plus à être mesurée en tera-octets.
Remarque. On aura noté que l’unité de base de mesure de la mémoire est l’octet (égal à 8 bits) et que les
préfixes usuels (kilo, méga, giga . . .) sont couramment utilisés. Cependant, ces derniers sont liés à la base 10 et
en réalité assez mal adaptés à un univers dans lequel la base 2 est reine. C’est pourquoi ont été inventés des
préfixes spécifiques à la mesure de la mémoire informatique. Partant du principe que 103 ≈ 210 , on utilise les
préfixes kibi, mébi, gibi . . . dont la définition est donnée figure 3.
Malheureusement ces notations sont assez récentes (), les premiers informaticiens s’étant contenté de
modifier l’usage des préfixes usuels, et nombreux sont encore ces derniers à parler de kilo-octet pour désigner
en réalité un kibi-octet. On notera cependant que si l’erreur est faible s’agissant de ko (2,4%), celle-ci augmente
significativement s’agissant de Go (7,4%) voire de To (10%).
Jean-Pierre Becirspahic
1.4 informatique commune
Lecture et écriture
Seul le processeur peut modifier l’état de la mémoire ; il peut :
– écrire à un emplacement (le processeur donne une valeur et une adresse et la mémoire range la valeur à
l’emplacement indiqué par l’adresse) ;
– lire un emplacement (le processeur demande à la mémoire la valeur contenue à l’emplacement dont il
indique l’adresse).
Notons que les opérations de lecture et d’écriture portent en général sur plusieurs octets contigus en mémoire,
qu’on appelle un mot mémoire. La taille d’un mot mémoire est de 4 octets pour un processeur 32 bits et de 8
octets pour un processeur 64 bits.
Figure 4 – Le temps d’accès est l’intervalle de temps entre la demande de lecture/écriture et la disponibilité de
la donnée.
1.3 Le processeur
Un processeur, ou encore CPU (Central Process Unit), est composé d’une unité de commande et d’une unité de
traitement.
L’unité de commande décode les instructions d’un programme et les transcrit en une succession d’instructions
élémentaires envoyées à l’unité de traitement. Son travail est cadencé par une horloge, ce qui permet de
coordonner les instructions et d’optimiser le traitement d’une succession d’instructions.
L’unité de traitement est composée d’une unité arithmétique et logique (UAL), d’un registre d’entrée et d’un
accumulateur 5 (figure 5).
bus de données
instruction UAL
5. Cette représentation est très simplifiée et ne correspond pas à tous les modèles de processeurs, mais les principes restent les mêmes.
Éléments d’architecture des ordinateurs 1.5
Suivant le contexte, un mot mémoire (une donnée) peut représenter un entier, une adresse, ou encore une
instruction transmise au processeur. Ainsi, un programme n’est qu’une suite d’instructions, autrement dit un
certain nombre de mots mémoire écrits en binaire. L’ensemble de ces instructions comprises par le processeur
s’appelle le langage machine ; pour en faciliter la lecture et l’écriture, on représente les mots binaires correspon-
dant aux instructions par des mnémoniques comme LOAD, ADD, JMP . . . ; on parle alors de langage assembleur ;
ce langage est en bijection stricte avec le langage machine.
01010 10000000011
programme 10111 01110000110
01110 11001100110
Mais si l’introduction de l’assembleur dans les années est la marque d’un progrès vers l’abstraction, un
défaut fondamental subsistait : un programme en assembleur est écrit en termes de ce que sait faire la machine ;
l’invention des langages de programmation va permettre de changer de paradigme en rédigeant un programme
en termes de ce que veut faire l’utilisateur. En outre, cette démarche présente l’avantage d’être indépendante
de la machine : un langage de programmation s’exprime en termes indépendants de la nature du processeur
présent dans l’ordinateur, contrairement à l’assembleur.
Jean-Pierre Becirspahic
1.6 informatique commune
un des facteurs qui explique sa longévité, car ce langage reste encore très utilisé à l’heure actuelle (il est vrai au
prix d’une importante évolution depuis sa naissance). D’ailleurs, certaines des bibliothèques mathématiques
que nous utiliserons cette année utilisent du code écrit en Fortran, même si nous ne nous en rendrons pas
compte.
Un langage compilé
Fortran est un langage compilé, c’est-à-dire qu’il nécessite l’usage d’un compilateur, un programme spécialisé
chargé de traduire un code source écrit dans un langage de haut niveau vers le langage machine. En général,
l’efficacité du code produit est considéré comme un facteur déterminant de qualité, et ce sont les efforts
constants consacrés à l’amélioration des compilateurs Fortran qui sont une des autres cause de la longévité de
ce langage.
Interprètes et compilateurs
Nous l’avons déjà dit, le seul langage compris par le processeur est le langage machine, langage composé
d’un ensemble restreint d’opérations élémentaires, dont l’exécution est implémentée directement dans les
composants du processeur. À l’inverse, un langage de programmation se place à un niveau d’abstraction
indépendant de la machine, mieux à même de traduire les objectifs du programmeur. Ceci implique la nécessité
d’utiliser une sorte de traducteur entre le langage de programmation et le langage machine. Schématiquement,
il en existe de deux types :
– un compilateur est un programme qui traduit un code écrit dans un langage de programmation en langage
machine. Le code produit (qu’on appelle un exécutable) peut être exécuté directement sur la machine sur
laquelle il a été compilé.
– un interprète est un programme qui simule une machine virtuelle dont le langage machine serait le
langage de programmation lui-même. Le code va être lu, analysé et exécuté instruction par instruction
par l’interprète.
machine virtuelle
interprète
.txt
code source
processeur
compilateur
.exe
exécutable
L’exécutable créé par le compilateur dépend de la machine sur lequel il a été créé, alors qu’un programme
interprété est indépendant de la machine (puisque c’est l’interprète lui-même qui dépend de la machine). En
général, un programme compilé est nettement plus rapide qu’un programme interprété puisque toute la phase
d’analyse et de vérification du code source a déjà été faite lors de la phase de compilation.
Éléments d’architecture des ordinateurs 1.7
• Algol et Cobol
À partir de un grand nombre de langages furent proposés, souvent pour répondre à des faiblesses du
Fortran. L’Algol (pour Algorithmic Language) est le fruit d’un travail universitaire visant à définir un
langage « universel » de programmation orienté vers le calcul scientifique, avec un accent sur la généralité, la
sécurité et la rigueur de la définition, points faibles du Fortran. Finalement assez peu utilisé, il a néanmoins
posé les bases de l’algorithmique et a influencé de nombreux langages plus récents : Pascal (), C () et
ses descendants, Java (), Python () et plus généralement la plus-part des langages impératifs actuels.
Dans un autre registre, le langage Cobol () a été créé pour répondre à un besoin croissant en informatique
de gestion, domaine dans lequel Fortran était peu adapté. Bien que présentant de nombreux défauts, il est
néanmoins resté pendant longtemps un langage de référence en matière d’informatique de gestion. Il n’a pas eu
de véritable descendant, mais a connu de nombreuses évolutions.
• Lisp
La naissance du Lisp en est importante à plus d’un titre. C’est tout d’abord le premier langage fonctionnel
inspiré du lambda-calcul de Church ; à ce titre on peut le considérer comme l’ancêtre de tous les langages
fonctionnels et en particulier de Caml (), le langage qui sera utilisé en option informatique. C’est aussi le
premier langage interprété.
• Python
Dans les années - les chercheurs en informatique poursuivent en vain le mythe du « langage universel »,
jusqu’à prendre conscience de la vanité de cette recherche. Il n’existe pas un mais des langages de programma-
tion, chacun ayant ses points forts et ses points faibles, ainsi que ses domaines d’application privilégiés.
Le langage que nous utiliserons pour illustrer ce cours est le Python (né en ). Il s’agit d’un langage de
style majoritairement impératif, utilisé dans de nombreux domaines : développement web et internet, calcul
numérique et scientifique, éducation. Il s’agit d’un langage interprété, à une nuance près : pour être plus efficace,
un script Python est tout d’abord compilé en un langage intermédiaire entre le langage de programmation et le
langage machine et qu’on appelle le bytecode. Ce dernier est ensuite interprété par une machine virtuelle, le
processus complet étant transparent pour l’utilisateur standard. Les performances des interprètes de bytecode
sont en général bien meilleures que celles des interprètes de langages de plus haut niveau car le bytecode est
déjà plus proche du langage machine et moins intelligible par l’homme, mais il reste indépendant de la machine
sur laquelle il a été créé.
script bytecode
machine virtuelle
L’interprète de bytecode le plus utilisé en Python s’appelle CPython ; il est écrit en C, mais d’autres existent
(Jython, écrit en Java par exemple).
2. Système d’exploitation
Le système d’exploitation est le premier programme exécuté lors de la mise en marche de l’ordinateur ; il permet
d’accéder aux ressources matérielles de celui-ci, en particulier les organes d’entrées/sorties et la mémoire de
masse (le disque dur). Le rôle principal du système d’exploitation est d’isoler les programmes des détails du
matériel : un programme désirant afficher une image ne va pas envoyer directement des instructions à la carte
graphique de l’ordinateur, mais plutôt demander au système d’exploitation de le faire. C’est ce dernier qui doit
connaitre les détails du matériel (dans ce cas le type de carte graphique et les instructions qu’elle comprend).
Cette répartition des rôles simplifie grandement l’écriture des programmes d’application.
Jean-Pierre Becirspahic
1.8 informatique commune
De cette première tâche découle une seconde : celle d’assurer l’interface entre le ou les utilisateurs et la machine,
en fournissant à chacun d’eux une machine virtuelle à travers laquelle chacun pourra interagir avec la machine.
Actuellement, les systèmes d’exploitation les plus connus appartiennent à l’une des deux familles suivantes : la
famille unix (Mac OS X et Linux pour les ordinateurs, iOS et Android pour les tablettes et smartphones) et la
famille windows. Dans la suite de cette partie, nous ne décrirons que certains aspects de la première de ces
deux familles, étant entendu que les principes généraux que nous allons évoquer sont partagés par tous les
systèmes d’exploitation actuels.
2.1 Unix
Au début des années , une équipe des laboratoires Bell donne naissance à un nouveau système d’exploi-
tation : Unix. Ce dernier est écrit dans un tout nouveau langage de programmation, le C, créé par un des
membres de cette équipe, Dennis Ritchie. Ce langage, proche de la machine physique (il permet notamment
de manipuler directement des adresses mémoires) et pourvu pour cette raison de compilateurs très efficaces,
devait par la suite devenir le langage privilégié pour l’écriture de logiciels destinés à interagir directement avec
la machine, et en particulier les systèmes d’exploitation.
Unix est organisé autour d’un noyau de base qui sert de support à un interprète de langage de commande, le
shell 6 . Pour l’utilisateur, ce langage de commande permet de réaliser des tâches complexes par assemblage
d’actions élémentaires par l’intermédiaire d’une interface textuelle (le terminal) ou graphique.
Chemin d’accès
Tout fichier est référencé par son chemin d’accès, c’est à dire la description du chemin qu’il faut parcourir dans
l’arborescence à partir d’un certain répertoire pour atteindre le fichier en question. Le chemin est spécifié par
les noms des répertoires séparés par le caractère « / » et suivi du nom du fichier.
doc1
Lorsqu’on commence la description du chemin par la racine, on dit que le chemin est absolu. Par exemple, l’un
des deux documents possédé par Alice est référencé par le chemin : /home/alice/doc1. Il n’a bien entendu
rien à voir avec le fichier /home/bob/travail/doc1 possédé par Bob 7 .
Il est aussi possible de décrire un fichier relativement à la position d’un autre répertoire ; on parle alors de
chemin relatif. Sous unix par exemple, le symbole « ~ » désigne le répertoire d’accueil d’un utilisateur. Lors
d’une session ouverte par Bob, le document qui se trouve dans le répertoire travail peut être décrit par :
~/travail/doc1. Enfin, un fichier peut être décrit relativement au répertoire courant. Ce dernier est représenté
par le caractère « . », et on peut remontrer dans la hiérarchie au répertoire père d’un répertoire donné en le
désignant par « .. ». Par exemple, si le répertoire courant est le répertoire loisir de Bob, on peut décrire le
premier des deux fichiers d’Alice par : ../../alice/doc1.
Il est possible d’explorer cette arborescente en utilisant un terminal (ou le powershell) et les quelques instruc-
tions suivantes 8 :
– pwd affiche le répertoire courant ;
– ls affiche le contenu du répertoire courant ;
– cd change le répertoire courant ;
– mv déplace (et renomme) un fichier ou un dossier ;
– cp copie (et renomme) un fichier ;
– rm supprime un fichier ;
– mkdir crée un nouveau répertoire (vide) ;
– rmdir supprime un répertoire vide.
Par exemple, si Alice veut créer un dossier public dans son répertoire courant et y déplacer le fichier doc2 en
le renommant doc3 elle écrira dans son terminal :
mkdir ~/public
mv ~/doc2 ~/public/doc3
7. Dans le cas d’un système windows, le séparateur est le caractère « \ » et les périphériques désignés par une lettre suivie du caractère
« : ». Un exemple de chemin d’accès dans ce cas serait C:home\alice\doc1.
8. Ce sont des instructions Unix mais elles sont aussi utilisables dans Powershell.
Jean-Pierre Becirspahic
1.10 informatique commune
Les répertoires possèdent des droits analogues, le droit de lecture permet de visualiser le contenu de ce
répertoire et le droit d’écriture d’ajouter, de renommer et de supprimer des éléments de ce répertoire, à
condition de pouvoir y accéder grace au droit d’exécution. Ainsi, les droits courants pour un répertoire vont être
rwx, r−x et −wx car il ne sert pas à grand chose de pouvoir lire ou modifier un répertoire si on ne peut y accéder.
Alice souhaite maintenant autoriser Bob à déposer des fichiers dans son répertoire public mais pas en voir le
contenu. Il lui suffit d’écrire dans son terminal :
Bob peut maintenant copier son fichier doc1 dans le répertoire public d’Alice en écrivant dans son propre
terminal :
cp ~/travail/doc1 ../alice/public
ls ../alice/public
• L’interprète de commande
Souvenons-nous que Python est un langage interprété (voir le chapitre précédent) : il est nécessaire de
disposer d’un interprète de commande pour traduire nos instructions Python en langage machine. Sous
Pyzo, l’interprète de commande se trouve dans le Shell. Si vous utilisez la dernière version de Pyzo deux
interprètes de commandes sont disponibles, et en fonction des réglages des préférences l’un des deux est lancé
automatiquement au démarrage.
– S’il s’agit de l’interprète de commande par défaut le shell ressemblera à ceci :
Python 3.4.2 |Continuum Analytics, Inc.| (default, Oct 21 2014, 17:42:20)
on darwin (64 bits).
This is the IEP interpreter with integrated event loop for TK.
Type 'help' for help, type '?' for a list of *magic* commands.
>>>
In [1]:
IPython est un interprète de commande amélioré offrant plus de fonctionnalités que l’interprète par défaut,
mais pour débuter n’importe lequel fera l’affaire.
Jean-Pierre Becirspahic
2.2 informatique commune
Quel que soit l’interprète utilisé, ce dernier se propose d’engager avec vous un dialogue interactif en vous
invitant à taper une première commande après le prompt >>> ou In [1]:
Répondons à l’invite en tapant "1 + 1" suivi de la touche "entrée" et observons sa réponse :
>>> 1 + 1 In [1]: 1 + 1
2 Out[1]: 2
>>> In [2]:
L’interprète de commande a exécuté notre instruction et retourné le résultat de celle-ci. Une fois la tâche
terminée, le prompt vous invite de nouveau à taper une instruction. Nous pouvons déjà observer que Ipython
numérote les instructions et distingue clairement votre solicitation (In) de sa réponse (Out). Poursuivons le
dialogue avec :
>>> In [3]:
L’interprète IPython nous permet ici de discerner une différence qui serait passée inaperçue avec l’interprète
de commande traditionnel. Toute fonction Python retourne un résultat (affiché derrière le Out), et certaines
d’entres-elles ont en plus un effet sur l’environnement 1 :
– l’instruction 1 + 1 a comme résultat 2 et n’a pas d’effet sur l’environnement ;
– l’instruction print('Hello world !') retourne une valeur particulière nommée None et a pour effet
d’afficher une chaîne de caractères dans le shell.
La valeur None est la valeur retournée par les instructions qui n’ont en quelque sorte « rien à renvoyer » ;
l’interprète IPython ne s’y trompe pas et n’affiche pas cette réponse, alors qu’en toute logique on pourrait
s’attendre à voir écrit dans le shell : Out[2]: None
La chaîne Hello world ! qui apparaît dans le shell n’est pas le résultat de l’instruction print mais l’effet de
celle-ci sur son environnement.
Notons que l’interprète standard ne s’y trompe pas non plus : le résultat de l’instruction (None, rappelons-le)
n’apparaît pas dans le shell, mais avec cet interprète il n’est pas possible visuellement de distinguer une réponse
d’un effet.
print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)
In [4]:
Cette fonction possède quatre paramètres optionnels (file, sep, end et flush) qui possèdent des valeurs par
défaut, valeurs utilisées lorsqu’on ne précise pas explicitement les valeurs que l’on souhaite attribuer à ces
paramètres. Le premier de ceux-ci, file, désigne le « lieu » vers lequel doit être dirigé le flux de caractères à
imprimer ; sa valeur par défaut est sys.stdout (pour system standard output) qui désigne la sortie standard, à
savoir l’écran pour la majorité des systèmes, et plus précisément ici le shell. Pour s’en convaincre, modifions
cette sortie en écrivant :
1. On appelle effet une interaction avec un périphérique de sortie ou une modification de la mémoire.
Introduction à Python et à son environnement 2.3
In [5]:
Comme on peut le constater, cette fois il n’y a plus d’affichage dans le shell ; en revanche si vous tentez
l’expérience vous constaterez qu’a été créé dans votre répertoire par défaut un nouveau fichier texte nommé
essai.txt qui contient la chaîne de caractères 'Hello world !'.
Exercice 1 Pour vérifier que vous avez bien compris cette différence entre résultat et effet, compléter l’état
du shell après chacune des solicitations suivantes :
In [6]:
In [6]:
• L’éditeur de texte
Ce mode interactif est idéal pour de courtes commandes mais ne convient pas dès lors que nous aurons à rédiger
des programmes. Dans ce cas, on utilise l’éditeur de texte fourni par IEP. Il possède nombre de fonctionnalités
des éditeurs de texte usuels mais est doté en plus de certaines fonctionnalités propres à la programmation
(numérotation des lignes, coloration syntaxique, indentation automatique, etc.). En contrepartie, pour être
utilisables par IEP vos fichiers texte devront respecter certaines contraintes : leurs noms devront se terminer
par le suffixe .py, leur contenu devra suivre les règles d’indentation propres à Python (on en reparlera plus
loin), ne comporter que des instructions Python (si vous souhaitez ajouter une ligne de commentaires celle-ci
devra débuter par le caractère #), etc.
Recopions donc les lignes suivantes dans l’éditeur :
print('Un nouveau calcul')
print('5 * 3 =', 5*3)
2 + 4
puis exécutons ce script (menu "Exécuter → Exécuter le fichier" ou mieux par l’intermédiaire du raccourci
clavier). Observons se qui se passe dans le panneau de l’interprète IPython :
In [6]: (executing lines 1 to 3 of "<tmp 1>")
Un nouveau calcul
5 * 3 = 15
In [7]:
Il y a là une différence importante avec l’utilisation directe de l’interprète : lorsqu’on exécute un script écrit
dans l’éditeur le résultat de ce dernier n’est pas retourné. Les effets des deux instructions print ont bien eu
lieu, mais le résultat du calcul 2 + 4 n’est pas visible, alors que si ce calcul avait été exécuté directement dans le
shell, nous aurions vu apparaître Out[6]: 6
Revenons dans le panneau d’édition et modifions son contenu pour le remplacer par le contenu suivant :
# calcul du nombre de secondes dans une journée
print('une journée a une durée égale à', 60 * 60 * 24, 'secondes')
Jean-Pierre Becirspahic
2.4 informatique commune
In [8]:
La première ligne du script n’a pas d’effet ; comme la majorité des langages de script, les commentaires Python
débutent caractère #. Qu’il soit utilisé comme premier caractère ou non, le # introduit un commentaire jusqu’à
la fin de la ligne. Il ne faut jamais hésiter à commenter un code (sans pour autant tomber dans un verbiage
creux) pour en faciliter la compréhension future.
Quant à la seconde ligne du script, elle vous montre que la fonction print peut posséder plusieurs arguments.
Exercice 2 Relire l’aide dédiée à la fonction print (page 2.2 de ce document) pour comprendre le rôle des
paramètres optionnels sep et end, puis deviner le résultat de l’exécution du script suivant :
• Pour résumer
Python peut être utilisé aussi bien en mode interactif qu’en mode script. Dans le premier cas, il y a un dialogue
entre l’utilisateur et l’interprète : les commandes entrées par l’utilisateur sont évaluées au fur et à mesure. Cette
première solution est pratique pour réaliser des prototypes, ainsi que pour tester tout ou partie d’un programme
ou plus simplement pour interagir aisément et rapidement avec des structures de données complexes.
Pour une utilisation en mode script, les instructions à évaluer par l’interprète sont sauvegardées, comme
n’importe quel programme informatique, dans un fichier. Dans ce second cas, l’utilisateur doit saisir l’intégralité
des instructions qu’il souhaite voir évaluer à l’aide de l’éditeur de texte, prévoir des effets pour afficher les
résultats souhaités s’il y en a, puis demander leur exécution à l’interprète.
La fonction type permet de connaître le type d’un objet. Par exemple, le type int est utilisé pour représenter en
machine les nombres entiers, le type str pour les chaînes de caractères :
In [1]: type(5)
Out[1]: int
In [2]: type('LLG')
Out[2]: str
L’objet None possède lui aussi un type (dont il est le seul représentant) :
In [3]: type(None)
Out[3]: NoneType
In [4]: type(print)
Out[4]: builtin_function_or_method
Introduction à Python et à son environnement 2.5
Par ailleurs, chaque objet possède un identifiant (accessible par la fonction id) qui est l’adresse mémoire où se
trouve stockée la représentation machine de l’objet :
In [5]: id(5)
Out[5]: 4297331360
In [6]: id(None)
Out[6]: 4297071472
In [7]: id(print)
Out[7]: 4298509576
Dans l’immense majorité des cas nous n’aurons que faire de ces identifiants aussi il n’est pas nécessaire
de mémoriser cette fonction. Néanmoins, elle nous sera utile lorsque nous chercherons à comprendre le
fonctionnement en mémoire de certains mécanismes simples.
x + y somme de x et y
x - y différence de x et y
x * y produit de x par y
x / y quotient de x par y
x ** y x puissance y
x // y quotient de la division euclidienne de x par y
x % y reste de la division euclidienne de x par y
abs(x) valeur absolue (ou module) de x
Il existe des langages de programmation fortement typés : dans un tel langage il est tout bonnement impossible
d’effectuer une opération mêlant des objets de types différents (c’est le cas par exemple de Caml, le langage de
l’option informatique). Ce n’est heureusement pas le cas du langage Python : si cela s’avère nécessaire pour
réaliser une opération, la conversion de type est automatique.
Observons quelques exemples de calculs :
2. Plus précisément ceux dont la représentation dyadique (c’est-à-dire en base 2) est infinie.
Jean-Pierre Becirspahic
2.6 informatique commune
In [1]: 4 * 5 In [3]: 4 * 5.
Out[1]: 20 Out[3]: 20.0
In [2]: 23 / 3 In [4]: 24 / 4
Out[2]: 7.666666666666667 Out[4]: 6.0
Le produit de deux entiers étant toujours entier, le premier calcul ne nécessite donc pas de conversion de type :
les deux arguments sont de type int, le résultat aussi.
Le quotient de deux entiers n’étant pas toujours entier, le second calcul nécessite de convertir au préalable les
deux arguments au type float. Le résultat affiché est donc celui de l’opération 23.0 / 3.0.
Le troisième calcul est le produit d’un objet de type int par un objet de type float. Le premier objet doit donc
être converti au type float avant que l’opération ne soit effectuée.
Notez bien que bien que le second opérande ainsi que le résultat représentent des entiers (au sens mathématique),
il n’y a pas de conversion automatique au type int. En effet, les conversions automatiques ne sont possibles que
dans le sens : int −→ float −→ complex. Dans les autres sens, la conversion doit être explicite (voir plus loin).
Enfin, le dernier calcul montre que même si le résultat de la division est entier (au sens mathématique du
terme), le résultat d’une opération faisant intervenir l’opérateur / sera toujours de type float (ou complex). Le
résultat affiché est celui de l’opération 24.0 / 4.0.
Division euclidienne
En informatique on utilise très régulièrement le quotient et le reste d’une division euclidienne : les opérations
associées se notent respectivement // et % :
23 3
reste 2 7 quotient
In [5]: 23 // 3 In [7]: 24 // 4
Out[5]: 7 Out[7]: 6
In [6]: 23 % 3 In [8]: 24 % 4
Out[6]: 2 Out[8]: 0
Ces deux opérations sont à privilégier lorsqu’on souhaite obtenir une réponse sous forme entière puisque le
calcul s’effectue exclusivement avec le type int 3 .
Comme on peut le constater, les conversions float −→ int (exemples 9 et 10) mais aussi int −→ float (exemple 13)
peuvent conduire à des résultats inexacts sur le plan mathématique.
3. Nous apprendrons plus tard que les opérations sur le type int donnent toujours des résultats exacts alors que les calculs sur le type
float sont toujours entachés d’une marge d’erreur.
4. À utiliser avec circonspection, donc.
Introduction à Python et à son environnement 2.7
Notons enfin qu’il n’est pas possible de convertir un type complex en type float ou int (si nécessaire prendre la
partie réelle pour obtenir un objet de type float).
In [15]: float(2 + 0j)
TypeError: can't convert complex to float
In [16]: (2 + 0j).real
Out[16]: 2.0
In [17]: (2+0j).imag
Out[17]: 0.0
Il existe deux modes d’importation d’un module. Par exemple, pour importer le module
numpy on peut :
1. utiliser l’instruction import numpy
Dans ce cas, toutes les fonctions de ce module devront être préfixées du nom du module
pour être utilisées : pour calculer un sinus il faudra utiliser la fonction numpy.sinus
Comme les noms des modules peuvent être longs on peut leur donner un nom d’usage
plus court en écrivant par exemple :
import numpy as np
Dans ce cas on calcule un sinus à l’aide de la fonction np.sin
2. utiliser l’instruction from numpy import *
Dans ce cas les fonctions n’ont pas besoin d’être préfixées par le nom du module : pour
calculer un sinus il suffit d’utiliser la fonction sin
Néanmoins ce mode d’importation n’est pas sans danger si on utilise plusieurs modules
différents qui contiennent des fonctions de même nom car dans ce cas, c’est la fonction
issue de la dernière importation qui sera utilisée a .
On peut diminuer ce risque en n’important que les fonctions dont on aura l’usage. Par
exemple, si on ne prévoit de n’utiliser que les fonctions trigonométriques on écrira :
from numpy import sin, cos, tan
a. Ce mode d’importation présente un autre défaut : il charge en mémoire l’intégralité des définitions contenues
dans ce module, alors que le mode précédent se contente de mémoriser le chemin menant au module en question.
S’agissant de fonctions à valeurs numériques, le résultat retourné est le plus souvent un objet de type float ou
complex.
Jean-Pierre Becirspahic
2.8 informatique commune
In [19]: np.sqrt(2)
Out[19]: 1.4142135623730951
In [20]: np.pi
Out[20]: 3.141592653589793
In [20]: np.sin(np.pi)
Out[20]: 1.2246467991473532e−16
2.2 Booléens
Le type bool est un type un peu particulier, puisqu’il ne comporte que deux objets : True et False, qui
représentent le vrai et le faux.
À ce type sont associés trois opérateurs : not, and et or, qui sont définis par les tables suivantes :
not and False True or False True
False True False False False False False True
True False True False True True True True
Par ailleurs, un certain nombre d’opérateurs sont définis sur d’autres types (en particulier les types de nombres)
et à valeurs dans le type bool (voir figure 4).
Ces différents opérateurs peuvent se combiner entre eux et ainsi, on retiendra qu’évaluer une expression
booléenne n’est pas autre chose que le résultat d’un calcul, à l’instar des calculs que l’on effectue sur les nombres.
In [1]: (4 + 3) < 11 and not 'alpha' > 'omega'
Out[1]: True
2.3 Variables
Les données calculées peuvent être mémorisées à l’aide de variables. Pour ce faire, il faut attribuer un nom à
cette variable et lui associer une valeur à l’aide de l’opérateur d’affectation =. Le nom de la variable est une suite
de lettres (minuscules ou majuscules) et de chiffres, qui doit toujours commencer par une lettre. Il est d’usage
de choisir des termes explicites pour faciliter la lecture du code.
Une fois affectée, la valeur de la variable peut être utilisée dans un calcul en faisant référence à son nom. Voici
un exemple de calcul de l’aire d’un rectangle dans lequel les noms de variables ont été choisis de manière à
rendre le code limpide :
Introduction à Python et à son environnement 2.9
Notez qu’une affectation réalise un effet (sur la mémoire) et retourne la valeur None. La création d’une variable
établit un lien entre sa référence (c’est à dire son nom) et un emplacement en mémoire qui contient la valeur de
celle-ci.
À l’issue de ce script la situation en mémoire 6 est présentée figure 5.
4577628496 : 525.141
Tableau de référencement
Nom Adresse
aire 4577628496 4577628400 : 12.45
largeur 4577628400
longueur 4577628352
4577628352 : 42.18
En général les utilisateurs que nous sommes n’ont que faire de l’adresse mémoire où sont stockées les valeurs
manipulées et celle-ci restera cachée, à moins d’utiliser la fonction id qui la renvoie :
In [5]: id(largeur)
Out[5]: 4577628400
Une fois définie, la valeur d’une variable peut être modifiée, toujours à l’aide de l’opérateur d’affectation. À titre
d’exemple, modifions la valeur de la variable largeur :
In [6]: largeur = 15.7
Il a été modifié, ce qui nous permet de deviner ce qui s’est passé : la nouvelle valeur 15, 7 a été stockée à l’adresse
4577628442 et le tableau de référencement modifié en conséquence (illustration figure 6). L’emplacement
mémoire contenant l’ancienne valeur 12, 45 n’est plus référencé et est réaffecté à l’espace mémoire disponible.
Notons que la valeur de la variable aire n’a pas été modifiée. Le calcul effectué ligne 3 utilise les valeurs des
variables au moment où il a été exécuté et n’établit en aucune manière une liaison particulière entre les variables
elles-mêmes.
Ce mécanisme permet de modifier le contenu d’une variable à l’aide de sa propre valeur, comme par exemple :
In [8]: longueur = longueur + 1
À l’issue de cette instruction la variable longueur aura pour valeur 43, 18 (et son identifiant sera différent). Mo-
difier une variable à l’aide de sa propre valeur est d’ailleurs une opération tellement fréquente en informatique
que cette instruction ligne 6 aurait pu s’écrire plus succinctement : longueur += 1.
6. Sur ce schéma, adresses et valeurs sont indiquées sous forme décimale pour des raisons de lisibilité.
Jean-Pierre Becirspahic
2.10 informatique commune
4577628496 : 525.141
4577628352 : 42.18
Remarque. Le recyclage de l’espace déréférencé est le fait d’un processus tournant en tâche de fond : le garbage
collector (ou ramasse-miettes en français). Ce programme détermine quels sont les objets en mémoire qui ne
peuvent plus être utilisés par l’interprète de commande (parce qu’ils ne sont plus référencés) et ré-affecte
l’espace qu’ils occupent à l’espace mémoire disponible. Notons que les langages de plus bas niveau (le C par
exemple) ne possèdent en général pas de ramasse-miettes, ce qui oblige le programmeur à allouer et libérer
lui-même la mémoire (ce qui en contrepartie permet une gestion plus fine qu’un processus automatique).
Affectations parallèles
Un problème classique auquel est confronté le programmeur débutant est la permutation du contenu de deux
variables a et b. En effet, la succession d’opérations :
b = a
a = b
ne convient pas car ces deux affectations sont effectuées séquentiellement et non pas simultanément. Pour s’en
convaincre, il suffit de représenter la modification des référencements après chacune de ces deux affectations
(voir figure 7).
À l’issue de ces deux instructions l’une des deux valeurs n’est plus référencée et l’autre l’est par les deux
variables a et b.
Heureusement il est possible en Python d’effectuer plusieurs applications simultanément en séparant les noms
et les valeurs correspondantes par des virgules, comme dans l’exemple ci-dessous :
In [9]: a, b = 42, 78.5
Exercice 3 S’il n’était pas possible d’effectuer plusieurs affectations simultanément, comment procéderiez-
vous pour permuter le contenu de a et b ?
Introduction à Python et à son environnement 2.11
In [11]: a = 257
a 257
In [12]: b = 257
In [13]: c = a
c
In [13]: a == b
Out[13]: True
b 257
In [14]: a is b
Out[13]: False
In [14]: a is c
Out[13]: True
Dans la majorité des cas nous n’aurons besoin que de l’égalité de valeur ; l’égalité physique ne nous sera utile
que pour illustrer certaines parties du cours.
In [1]: "aujourd'hui"
Out[1]: "aujourd'hui"
Certains caractères spéciaux se définissent à l’aide du caractère d’échappement \ (le backslash) ; c’est le cas par
exemple du caractère spécial interprété comme un passage à la ligne, représenté par \n :
(notez que l’espace qui suit le passage à la ligne compte comme un caractère).
Comme on peut le constater, la fonction print reconnait de tels caractères et les affiche correctement.
À beaucoup d’égards, une chaîne de caractère ne diffère pas d’une donnée numérique : on peut réaliser des
opérations sur les chaînes, même si dans le cas présent ces opérations se réduisent à la concaténation représenté
par l’opérateur + et à la duplication par l’opérateur *. Par ailleurs on peut assigner une chaîne de caractères à
une variable :
Jean-Pierre Becirspahic
2.12 informatique commune
In [6]: print(chn)
Hello world !
Attention à bien faire la différence entre la chaîne de caractère '123' (de type str ) et l’entier 123 (de type int ) ;
ces deux objets sont représentés différemment en mémoire et l’opérateur + ne va pas réagir de la même façon :
In [7]: '123' + '1' # concaténation de deux chaînes
Out[7]: '1231'
In [9]: '123' + 1
TypeError: cannot concatenate 'str' and 'int' objects
Le troisième exemple illustre une erreur de typage : il n’existe pas d’opérateur noté + entre une chaîne de
caractères et un entier.
Contrairement à ce que nous avons pu observer entre le type int et le type float , il n’existe pas ici de conversion
automatique de type entre le type int et le type str , mais les fonctions int et str permettent de réaliser
explicitement cette conversion :
In [10]: int('123') + 1
Out[10]: 124
Il en est de même de la fonction float, qui permet de convertir une donnée adéquate (entier ou chaîne de
caractères) en un nombre flottant.
In [13]: ch[4]
Out[13]: 's'
Il est aussi possible d’indexer les caractères par des entiers négatifs ce qui revient à compter à partir de la fin (le
dernier caractère de la chaîne est alors d’indice −1) :
0 1 2 3 4 5 6 7 8 9 10 11 12 13
L o u i s - L e - G r a n d
−14 −13 −12 −11 −10 −9 −8 −7 −6 −5 −4 −3 −2 −1
Notons enfin que la fonction len permet de calculer la longueur d’une chaîne de caractères :
Introduction à Python et à son environnement 2.13
In [16]: len(ch)
Out[16]: 14
Slicing
Plus généralement, la technique sur slicing (littéralement le découpage en tranches) permet d’extraire une portion
d’une chaîne de caractères : il suffit de préciser l’indice de début i (qui sera inclus dans le découpage) et l’indice
de fin j (qui sera exclus) sous la forme [i:j].
In [17]: ch[3:−3]
Out[17]: 'is−Le−Gr'
Si l’indice i n’est pas précisé, il est implicitement pris égal à 0 ; de même si j n’est pas précisé il sera égal à la
longueur de la chaîne :
In [18]: ch[6:] + ch[:6]
Out[18]: 'Le−GrandLouis−'
Comme on peut le constater, avec cette technique il est très simple d’extraire les k premiers caractères d’une
chaîne : il suffit d’écrire ch[:k]. De même, pour extraire les k derniers caractères il suffit d’écrire ch[−k:].
Enfin, le slicing dans sa forme la plus générale prend un troisième paramètre correspondant au pas de la
sélection : ch[i:j:k] retourne tous les caractères dont les indices sont compris entre i (inclus) et j (exclus) et
séparés d’un pas k. Par exemple, pour obtenir les caractères d’indices pairs puis les caractères d’indices impairs
on écrira :
In [19]: ch[::2]
Out[19]: 'LusL−rn'
In [20]: ch[1::2]
Out[20]: 'oi−eGad'
Enfin, notons que le pas peut aussi prendre des valeurs négatives, ce qui nous donne un moyen simple d’inverser
les caractères d’une chaîne :
In [21]: ch[::−1]
Out[21]: 'dnarG−eL−siuoL'
(Lorsque le pas est négatif, les valeurs par défaut de i et j sont respectivement −1 et −ln(ch)−1.)
Exercice 4 Le mélange de Monge d’un paquet de cartes numérotées de 1 à 2n consiste à démarrer un nouveau
paquet avec la carte 1, à placer la carte 2 au dessus de ce nouveau paquet, puis la carte 3 au dessous du nouveau
paquet et ainsi de suite en plaçant les cartes paires au dessus du nouveau paquet et les cartes impaires au
dessous.
Autrement dit, si le paquet de cartes initial est représenté par la suite (1, 2, 3, . . . , 2n), son mélange de Monge
sera représenté par la suite (2n, 2n − 2, . . . , 4, 2, 1, 3, 5, . . . , 2n − 3, 2n − 1).
Réaliser en une ligne Python le calcul d’un mélange de Monge d’une chaîne de caractères s.
Jean-Pierre Becirspahic
informatique commune
Chapitre 3
Instructions itératives
1. Structuration et indentation
Les impératifs de la programmation structurée nécessitent la définition de blocs d’instructions au sein des
structures de contrôles (def, for, while, if, . . .). Certains langages utilisent des délimiteurs pour encadrer ces
blocs d’instructions (des parenthèses en C ou en Caml, des mots-clés en Fortran, etc), mais le langage Python
se distingue en utilisant l’indentation, qui favorise la lisibilité du code.
Le début d’un bloc d’instructions est défini par un double-point (:), la première ligne pouvant être considérée
comme un en-tête. Le corps du bloc est alors indenté d’un nombre d’espaces fixes (quatre par défaut), et le
retour à l’indentation de l’en-tête marque la fin du bloc.
en−tête:
bloc ...........................
................................
d'instructions .................
Il est possible d’imbriquer des blocs d’instructions les uns dans les autres :
en−tête 1:
................................
................................
en−tête 2:
bloc .......................
............................
d'instructions .............
................................
................................
Cette structuration sert entre autre à définir de nouvelles fonctions, à réaliser des tests ou à effectuer des
instructions répétitives.
Dans l’histoire de l’informatique on distingue traditionnellement deux types de routines 1 : les procédures, qui
ne retournent pas de résultat et se contentent d’agir sur l’environnement, et les fonctions proprement dites, qui
retournent un résultat (en général par le biais d’une instruction return). En Python cette distinction n’existe
pas car il n’existe que des fonctions : les procédures ne sont que des fonctions particulières qui retournent la
valeur spéciale None lorsque l’instruction return n’est pas utilisée.
Il est facile de faire la distinction en utilisant l’interprète de commande IPython : lorsqu’on applique une
fonction qui retourne un résultat, ce dernier est précisé à la suite du mot Out[..]. Par exemple, print est une
procédure et len une fonction :
1. Une routine est une séquence d’instructions qui peut être réutilisée au sein d’un programme.
Jean-Pierre Becirspahic
3.2 informatique commune
On notera que bien que la procédure print retourne la valeur None, celle-ci est ignorée par l’interprète de
commande.
Exemple d’utilisation :
In [1]: norme(3, 4)
Out[1]: 5.0
Vous apprendrez plus tard en cours de mathématique qu’il existe d’autres normes, en particulier celles-ci :
Nk (x, y) = (xk + y k )1/k , la norme euclidienne correspondant au cas k = 2. Pour les définir, il suffit de passer k en
argument :
In [3]: norme(3, 4, 3)
Out[3]: 4.497941445275415
In [4]: norme(3, 4)
TypeError: norme() takes exactly 3 arguments (2 given)
Arguments optionnels
On peut constater sur ce dernier exemple qu’une erreur se produit si on ne donne pas exactement le bon nombre
d’arguments d’une fonction. Il est possible de modifier cet état de fait en précisant les valeurs par défaut que
doivent prendre les arguments d’une fonction : il suffit de préciser dans la liste des paramètres les valeurs
prises par défaut :
Avec cette nouvelle définition, si on omet de préciser le troisième paramètre, ce dernier sera pris égal à 2 :
In [5]: norme(3, 4, 3) # ici k=3
Out[5]: 4.497941445275415
2. voire n’en posséder aucun, auquel cas la liste des arguments reste vide ().
Instructions itératives 3.3
Attention, dans le cas d’une fonction avec des paramètres optionnels, les arguments doivent être ordonnés : les
paramètres optionnels doivent suivre les paramètres obligatoires. Il est d’ailleurs préférable de les nommer
pour éviter toute ambiguïté ; ainsi il est conseillé d’écrire l’instruction de la ligne 5 ainsi :
In [7]: norme(3, 4, k=3)
Out[7]: 4.497941445275415
Enfin, certaines fonctions peuvent avoir un nombre arbitraire de paramètres. Dans ce cas, les paramètres
optionnels doivent obligatoirement être nommés. C’est le cas par exemple de la fonction print qui possède
deux paramètres optionnels sep (valeur par défaut : un espace) qui est inséré entre chacun des arguments de la
fonction et end (valeur par défaut : un passage à la ligne) qui est ajouté à la fin du dernier des arguments :
In [8]: print(1, 2, 3, sep='+', end='=6\n')
1+2+3=6
In [11]: f()
Out[11]: 1
In [12]: b
NameError: name 'b' is not defined
Il est même possible qu’une variable locale ait le même nom qu’une variable globale ; ce n’est vraiment pas
souhaitable car générateur d’erreurs, mais sachez que par défaut, les variables définies à l’intérieur de la
définition d’une fonction sont supposées locales. On s’en convaincra avec l’expérience présentée figure 2.
In [14]: g()
Out[14]: 2
Pour distinguer une variable locale d’une variable globale au sein de la définition d’une fonction, il faut suivre
la règle suivante :
– si une variable se voit assigner une nouvelle valeur à l’intérieur de la fonction, cette variable est considérée
comme locale (c’est le cas de b dans la définition de la ligne 10) ;
Jean-Pierre Becirspahic
3.4 informatique commune
– si on se contente de faire appel au référencement d’une variable au sein d’une fonction, cette variable est
considérée comme globale (c’est le cas de a dans la définition de la ligne 10).
Si vraiment on souhaite modifier le contenu d’une variable globale à l’intérieur du bloc d’instructions d’une fonc-
tion, il faut utiliser l’instruction global pour déclarer celles des variables qui doivent être traitées globalement
(illustration figure 3).
In [17]: h()
Out[17]: 2
Figure 3 – Une variable globale peut être modifiée au sein d’une fonction.
Cependant, il est déconseillé d’utiliser des variables globales, car leur usage favorise la programmation spaghetti 3 :
elles compliquent la compréhension et la modification d’un script dès lors que ce dernier devient un peu long.
En effet, la présence de variables globales empêche le programmeur de restreindre son analyse à une petite
partie du code car une variable globale peut avoir été définie ou modifiée à tout endroit du script. Toute
modification ou débogage demande donc d’analyser le code dans son entier. Il est bien souvent préférable
d’utiliser au sein des fonctions des paramètres avec des valeurs par défaut plutôt que des variables globales.
L’exercice suivant devrait vous convaincre des difficultés que peuvent engendrer l’usage de variables globales
lorsque celle-ci sont modifiées :
Exercice 1 On considère les trois fonctions :
def f(): def g():
def h():
global a a = 1
a = a + 1
a = a + 1 a = a + 1
return a
return a return a
a = 1
print(f(), a)
print(a, f())
print(a, g())
print(a, h())
if expression booléenne:
bloc..............
d'instructions 1..
else:
bloc..............
d'instructions 2..
(Notez bien l’indentation qui permet de délimiter chacun des deux blocs d’instructions.)
Le fonctionnement de cette instruction est le suivant : si l’expression booléenne de la première ligne s’évalue en
True, le premier bloc d’instructions est exécuté, si elle s’évalue en False c’est le second bloc qui est exécuté.
3. On qualifie ainsi et de manière péjorative un code désordonné, à l’image d’un plat de spaghettis : il suffit de tirer sur un fil d’un côté
de l’assiette pour que l’enchevêtrement des fils provoque des mouvements jusqu’au côté opposé.
Instructions itératives 3.5
Notez que l’instruction else est optionnelle si aucune instruction ne doit être réalisée dans le cas d’un test
négatif.
Rappelons qu’outre les opérateurs booléens not, and et or, les opérateurs suivants sont à valeurs booléennes :
x < y (x est strictement plus petit que y) ;
x > y (x est strictement plus grand que y) ;
x <= y (x est inférieur ou égal à y) ;
x >= y (x est supérieur ou égal à y) ;
x == y (x est égal à y) ;
x != y (x est différent de y).
On observera que le test d’égalité utilise les caractères == pour ne pas être confondu avec l’opérateur d’affecta-
tion.
Ces opérateurs permettent de comparer des nombres (de type int ou float mais également des chaînes de
caractères, comparées suivant l’ordre lexicographique :
In [1]: 'alpha' < 'omega'
Out[1]: True
Notons enfin que l’opérateur == est plus général encore puisqu’il permet de tester l’égalité de valeur 4 entre deux
objets Python quelconque.
2. Instructions itératives
Réaliser une itération, ou encore une boucle, c’est répéter un certain nombre de fois des instructions semblables.
Dans la plupart des langages de programmation, il existe deux instructions pour réaliser une boucle, suivant
qu’on peut calculer à l’avance le nombre d’itérations à réaliser (on parle alors de boucles énumérées) ou que le
nombre d’itérations dépend de la réalisation ou non d’une certaine condition (on parle dans ce cas de boucles
conditionnelles) ; le langage Python ne fait pas exception à la règle.
4. À ne pas confondre avec l’égalité physique qui se teste à l’aide de l’opérateur is (voir le chapitre précédent).
Jean-Pierre Becirspahic
3.6 informatique commune
• Boucles indexées
On définit une boucle indexée à l’aide de la fonction for, en suivant la structure suivante :
for ... in range(...):
bloc ...........................
................................
d'instructions .................
Immédiatement après le mot-clé for doit figurer le nom d’une variable, qui va prendre les différentes valeurs
de l’énumération produite par l’instruction range. Pour chacune de ces valeurs, le bloc d’instructions qui suit
sera exécuté.
Un premier exemple très simple :
In [4]: for x in range(2, 10, 3):
...: print(x, x**2)
2 4
5 25
8 64
Il est bien entendu possible d’imbriquer des boucles à l’intérieur d’autres boucles ; attention seulement à
respecter les règles d’indentation pour délimiter chacun des blocs d’instructions :
In [5]: for x in range(1, 6):
...: for y in range(1, 6):
...: print(x * y, end=' ')
...: print('/')
1 2 3 4 5 /
2 4 6 8 10 /
3 6 9 12 15 /
4 8 12 16 20 /
5 10 15 20 25 /
• Invariant de boucle
On appelle invariant de boucle toute assertion vérifiant les conditions suivantes :
Initialisation cette assertion est vraie avant la première itération de la boucle ;
Conservation si cette assertion est vraie avant une itération de la boucle, elle le reste avant l’itération suivante ;
5. Une liste est une structure de données que nous étudierons en détail dans un chapitre ultérieur ; pour l’instant, retenons seulement
qu’une liste est une collection ordonnée d’objets.
Instructions itératives 3.7
Terminaison une fois la boucle terminée, l’invariant fournit une propriété utile qui aide à établir/prouver/a-
nalyser l’algorithme.
Comme l’indique la troisième propriété, un invariant de boucle peut être utilisé en amont de la rédaction
d’un algorithme pour rédiger ce dernier sans erreur ; il peut aussi servir à prouver la validité d’un algorithme
existant, et enfin il peut servir à analyser un algorithme pour en découvrir le rôle.
Exemple. Commençons par un exemple simple : on souhaite étudier la suite (un )n∈N définie par la donnée de
la valeur initiale u0 = 0 et de la relation de récurrence uk+1 = 2uk + 1 en définissant une fonction Python qui
permette le calcul du terme de rang n de cette suite.
Considérons l’expression Python suivante : x = 2 * x + 1 . Si la variable x référence initialement la valeur de
uk , après cette instruction cette variable référencera la valeur de uk+1 . D’où l’idée, pour calculer un , d’initialiser
la variable x avec la valeur de u0 puis d’appliquer n fois cette instruction. Ceci conduit à définir la fonction
suivante :
def u(n):
x = 0
for k in range(n):
x = 2 * x + 1
return x
Dans ce cas, on choisira pour prouver la validité de cette fonction d’énoncer l’invariant suivant :
Une fois cet invariant énoncé, il devient évident que le corps de la boucle doit contenir l’instruction :
x = x * (k + 1) . Pour respecter l’initialisation, il est nécessaire que la variable x référence 0! = 1, ce
qui conduit à la rédaction de la fonction :
def fact(n):
x = 1
for k in range(n):
x = x * (k + 1)
return x
Exemple. Considérons enfin le calcul du terme de rang n de la suite de Fibonacci, définie par la donnée des
valeurs initiales u0 = 0, u1 = 1 et la relation de récurrence uk+2 = uk+1 + uk .
Compte tenu des deux exemples précédents, nous allons chercher à respecter l’invariant :
Mais un problème apparaît rapidement : pour respecter ce seul invariant, il faut être capable de calculer uk+1 à
l’aide de la seule valeur de uk , ce qui semble difficile, pour ne pas dire impossible.
Énoncer cet invariant nous permet de mettre en évidence la nécessité de référencer en même temps que uk la
valeur de uk−1 . Ceci nous conduit à utiliser une deuxième variable y et à adopter l’invariant suivant :
à l’entrée de la boucle indexée par k, la variable x référence la valeur de uk et la variable y la valeur de uk−1 ;
Pour respecter ce nouvel invariant, il suffit que le corps de la boucle contienne l’instruction x, y = x + y, x
et que les valeurs initiales de ces deux variables soient respectivement égales à u0 = 0 et u−1 = 1. D’où la
définition :
Jean-Pierre Becirspahic
3.8 informatique commune
def fib(n):
x, y = 0, 1
for k in range(n):
x, y = x + y, x
return x
Exercice 3 Les exemples précédents ont montré comment l’énoncé d’un invariant en amont de la rédaction
de l’algorithme pouvait servir à rédiger et à prouver la validité de ce dernier. Mais la recherche d’un invariant
peut aussi servir à analyser un algorithme pour en deviner le rôle.
n−1
X
On considère un polynôme p(x) = ai xi , représenté en Python par le tableau p = [a0, a1, a2, ...].
i=0
Déterminer un invariant pour établir le rôle de la fonction mystère ci-dessous :
dans laquelle ce qui suit l’instruction in doit être une structure de données énumérable. Outre les énumérations
engendrées la fonction range, nous avons déjà rencontré un autre exemple d’une telle structure de données : les
chaînes de caractères 6 . Dans ce cas, la variable qui suit l’instruction for va prendre successivement la valeur
des différents caractères qui composent cette chaîne.
Ce mécanisme est un des aspects remarquables du langage python, là où de nombreux langages de programma-
tion n’autorisent que des itérations suivant une progression arithmétique. On appréciera la différence figure 4.
In [1]: epeler('Louis−Le−Grand')
L o u i s − L e − G r a n d
Outre les intervalles et les chaînes de caractères, les listes, les tuples, les dictionnaires, les ensembles, les fichiers
(et d’autres encore) sont des objets énumérables.
Autres itérateurs
Il est parfois nécessaire de connaître à la fois l’indice et la valeur d’un élément d’un objet énumérable ; on utilise
dans ce cas l’instruction enumerate, qui retourne un couple formé de l’indice et de l’objet qui lui est associé :
6. Les listes, déjà évoquées dans une note précédente, en sont un autre exemple.
Instructions itératives 3.9
Enfin, il est possible d’énumérer deux énumérables en parallèle à l’aide de l’instruction zip (l’énumération se
termine quand l’énumération du plus petit des deux énumérables est achevée).
while condition:
bloc ...........................
................................
d'instructions .................
In [1]: while 1 + 1 == 3:
...: print('abc', end=' ')
...: print('def')
def
L’instruction print('abc', end='') n’est jamais exécutée puisque la condition est fausse.
En revanche, on évitera d’écrire dans l’interprète les instructions suivantes :
In [2]: while 1 + 1 == 2:
...: print('abc', end=' ')
...: print('def')
abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc
abc abc abc abc abc abc abc abc abc abc abc abc abc abc abc ...
car la condition étant éternellement vérifiée cette suite d’instructions conduit à bloquer l’interprète.
Ainsi, sauf exception la condition dans une telle boucle va dépendre d’une variable au moins dont le contenu
sera susceptible d’être modifié dans le corps de la boucle. Par exemple :
In [3]: x = 10
Jean-Pierre Becirspahic
3.10 informatique commune
et cherchons à déterminer son rôle (on suppose que les arguments a et b sont des entiers strictement positifs).
À l’évidence, la boucle conditionnelle réalise l’itération de deux suites (qn )n∈N et (rn )n∈N définies par la donnée
des valeurs initiales q0 = 0 et r0 = a et les relations : qn+1 = qn + 1 et rn+1 = rn − b. Il s’agit de deux suites
arithmétiques, on en déduit l’invariant suivant :
Sachant que lim(a − nb) = −∞, il existe un entier n vérifiant a − nb < b, ce qui prouve la terminaison de cette
fonction.
En outre, on peut affirmer que cette fonction retourne la valeur d’un couple (q, r) vérifiant : a = qb + r et
r < b 6 r + b, soit encore 0 6 r < b. Autrement dit, cette fonction calcule le quotient et le reste de la division
euclidienne de a par b.
Exercice 4 En déterminant un invariant, donner le rôle de la fonction suivante :
def mystere(n):
i, s = 0, 0
while s < n:
s += 2 * i + 1
i += 1
return i
Si le caractère c ne figure pas dans la chaîne chn, celle-ci sera parcourue dans son entier, mais s’il figure dans la
chaîne, le parcours sera interrompu dès la première occurrence rencontrée.
La fonction break quant à elle, permet d’interrompre le déroulement des instructions du bloc interne à la
boucle pour passer à la suite. En règle générale, on évite d’employer à tort et à travers cette instruction car elle a
souvent pour effet de rendre moins claires les conditions d’arrêt ; on fera néanmoins des exceptions le script
s’en trouvera notablement simplifié.
C’est le cas en particulier lorsque la boucle consiste à attendre le déclenchement d’un événement exceptionnel.
Pour illustrer cette situation, nous allons chercher à évaluer le nombre moyen de jets de deux dés à six faces
qu’il faut réaliser avant d’obtenir un double-six.
Pour simuler un jet de dé, nous allons utiliser la fonction randint(1, 7) qui retourne un entier pris au hasard
entre 1 et 6 (la fonction randint se trouve dans le module numpy.random ; il faut en charger la définition en
mémoire au préalable). Le script qui consiste à simuler un jet de deux dés jusqu’à obtenir un double-six est :
Instructions itératives 3.11
s = 0
while True:
s += 1
if randint(1,7) == 6 and randint(1,7) == 6:
break
Cette boucle a pour effet d’itérer la variable s jusqu’à la réalisation de l’événement attendu : un double-six.
Pour obtenir le nombre moyen de jets nécessaire à la réalisation de cet événement, il suffit de réaliser cette
expérience un nombre suffisant de fois puis de calculer la moyenne de la valeur prise par s au sortir de cette
boucle.
def test(n):
e = 0
for k in range(n):
s = 0
while True:
s += 1
if randint(1, 7) == 6 and randint(1, 7) == 6:
break
e += s
return e / n
In [1]: test(100000)
Out[1]: 35.96509
In [2]: test(100000)
Out[2]: 36.19394
In [3]: test(100000)
Out[3]: 36.00176
In [4]: test(100000)
Out[4]: 35.98994
Jean-Pierre Becirspahic
informatique commune
Chapitre 4
Représentation des nombres
Dans ce chapitre, nous allons nous intéresser à la façon dont un nombre (entier ou réel) peut être représenté à
l’intérieur d’un ordinateur. Nous savons déjà 1 que la mémoire des ordinateurs est découpée en blocs de 8 bits
(un octet) et qu’un processeur 64 bits va travailler avec des paquets de 8 octets (contre 4 pour les processeurs
32 bits). Nous disposons de 64 bits pour représenter un nombre ; il est donc naturel de faire intervenir la
décomposition de ce dernier en base 2 pour le représenter en mémoire.
Écriture en base 2
Dans le cas particulier de la base 2, seuls les chiffres 0 et 1 sont donc utilisés, ce qui rend les opérations usuelles
particulièrement simples à réaliser. En effet, les algorithmes de calcul appris à l’école primaire en base 10
(addition, soustraction, multiplication, division) se généralisent en base 2.
1 0 1 0
× 1 0 1
1 0 1 1 0 1 1 1 0 1 0
+ 1 0 1 0 0 1 1 0 1 0 · ·
1 0 0 0 0 1 0 0 1 1 0 0 1 0
Exercice 1 Réaliser les opérations suivantes en base 2, sans passer par la base 10, à l’aide des algorithmes
appris à l’école primaire :
Changement de base p
X
Pour convertir le nombre x = (ap ap−1 · · · a1 a0 )b en base 10, il suffit d’appliquer la formule x = ak bk . Ceci peut
être aisément effectué à l’aide du script python suivant : k=0
1. voir le chapitre 1.
2. on conviendra que par défaut les nombres sont écrits en base 10 ; ainsi on écrira 584 en lieu et place de (584)10 .
Jean-Pierre Becirspahic
4.2 informatique commune
Attention, pour définir un nombre dans une base non décimale on ne peut utiliser le type int car tout nombre
entré au clavier est implicitement supposé écrit en base 10. C’est la raison pour laquelle, dans cette fonction, les
nombres que l’on souhaite convertir sont représentés par une chaîne de caractères, et la fonction int permet de
les convertir en leur équivalent numérique. Cette fonction ne permet donc pas d’utiliser des bases supérieures à
10 puisque seuls les caractères de '0' à '9' peuvent être convertis en leur équivalent numérique.
Par exemple :
In [1]: base10('210122', b=3)
Out[1]: 584
In [2]: base10('1011011')
Out[2]: 91
On peut observer que la réponse numérique obtenue, indépendamment de sa représentation machine dont on parlera
plus tard, est implicitement représentée en base 10 dans la console. Pour des raisons évidentes, l’interaction
numérique entre la machine et l’utilisateur se fait en base 10.
Schéma de Horner
Il est possible d’effectuer ce calcul sans calcul de puissance en appliquant la méthode de Horner. Cette dernière
consiste à itérer la suite finie (u0 , u1 , . . . , up ) définie par :
u0 = (ap )b , u1 = (ap ap−1 )b , ··· uk = (ap ap−1 · · · ap−k )b , ··· up = (ap ap−1 · · · a0 )b = x.
Les termes de cette suite sont liés par la récurrence uk = buk−1 + ap−k , ce qui conduit à la définition alternative
(et préférable) suivante :
Conversion réciproque
À l’inverse, pour convertir un entier écrit en base 10 en une base quelconque, il faut observer que si x =
(ap ap−1 · · · a1 a0 )b alors le quotient de la division euclidienne de x par b est égal à q = (ap ap−1 · · · a1 )b et le reste à
r = a0 puisque x = bq + r avec 0 6 r 6 b − 1. Ceci conduit à la fonction suivante :
Par exemple :
In [3]: baseb(584, b=3)
Out[3]: '210122'
In [4]: baseb(91)
Out[4]: '1011011'
La seconde exception est la base 16, en faisant précéder le nombre des caractères 0x :
In [6]: 0xa27c
Out[6]: 41596
Pour écrire un nombre en base 16, nous avons besoin d’un caractère pour chacun des entiers de 0 à 15 ; on
complète donc les chiffres de 0 à 9 par les lettres a, b, c, d, e et f. Ainsi, (a)16 = 10, (b)16 = 11, (c)16 = 12,
(d)16 = 13, (e)16 = 14, (f)16 = 15.
Sachant que 24 = 16, tout nombre écrit en base 2 à l’aide de 4 chiffres s’écrit en base 16 à l’aide d’un seul chiffre :
Aussi, pour convertir un nombre quelconque de la base 2 à la base 16, il suffit de regrouper les chiffres qui
le composent par paquet de 4 et convertir chacun de ces paquets en un chiffre en base 16. Par exemple,
(1011 0110 1110 1001)2 = (b6e9)16 .
L’écriture hexadécimale (c’est-à-dire en base 16) est fréquemment utilisée en informatique car un octet (qui
rappelons le vaut 8 bits) sera toujours représenté par deux caractères hexadécimaux. Par exemple, une couleur
d’une page web est définie par trois octets représentant ses composantes RVB 3 . Ainsi, un navigateur web va
interpréter le code couleur (ffa500)16 comme du orange, (00ff00)16 comme du vert, ou encore (ee82ee)16
comme du violet. Potentiellement, 2563 = 16 777 216 couleurs différentes sont accessibles.
In [8]: hex(41397)
Out[8]: '0xa1b5'
Jean-Pierre Becirspahic
4.4 informatique commune
Deux valeurs particulières demandent à être bien connues, la représentation des entiers de la forme 2p et ceux
de la forme 2p − 1 :
– l’entier naturel 2p est représenté par 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
p bits
p bits
représentation binaire de x
Sur une architecture 64 bits le plus grand entier relatif est donc égal à 263 − 1, qu’on peut écrire en base 16 sous
la forme (7 fff · · · ffff)16 . En base 10, il vaut :
| {z }
15 chiffres f
In [1]: 0x7fffffffffffffff
Out[1]: 9223372036854775807
représentation binaire de 2n + x
Pour que cette représentation sur n bits commence par un 1, il est donc nécessaire que 2n + x soit compris entre
2n−1 = (1 000 0000 · · · 0000)2 et 2n − 1 = (1 111 1111 · · · 1111)2 soit :
| {z } | {z }
n − 1 chiffres n − 1 chiffres
2n−1 6 2n + x 6 2n − 1 ⇐⇒ −2n−1 6 x 6 −1
Le plus petit entier négatif représentable sur une architecture à n bits est donc égal à −2n−1 et est représenté
par : (1 000 · · · 0000)2 .
| {z }
n − 1 bits
Ainsi, les entiers relatifs représentables sur une architecture à n bits sont compris entre −2n−1 et 2n−1 − 1.
Représentation des nombres 4.5
Attention donc à bien distinguer un nombre de sa représentation, qui ne coïncide pas avec sa représentation
binaire dans le cas des nombres négatifs.
Exemple. Pour simplifier les calculs, considérons une représentation sur 8 bits (n = 8).
L’entier relatif 105 est représenté par l’octet 01101001, ce qui correspond à la représentation en base 2 de
l’entier 105 = 26 + 25 + 23 + 20 .
L’entier relatif −105 est représenté par l’octet 10010111, ce qui correspond à la représentation en base 2 de
l’entier 256 − 105 = 151 = 27 + 24 + 22 + 21 + 20 .
Exercice 2 Dans une représentation en complément à deux sur 8 bits, quels sont les entiers relatifs représen-
tés par 01101101 et par 10010010 ?
nombre représentation
0 0000......0000
1 0000......0001
··· ..............
2n−1 − 1 0111......1111
−2n−1 1000......0000
··· ..............
−1 1111......1111
Figure 4 – Les entiers relatifs codés sur n bits, classés par représentation croissante.
1 0 1 0 0 1 0 1
+ 0 1 1 1 0 0 0 1
1 0 0 0 1 0 1 1 0
On ne garde que les 8 derniers bits, à savoir 00010110 ; le premier bit est un 0 donc le résultat de la somme est
positif ; il représente donc l’entier (10110)2 = 22, qui est bien le résultat de l’addition de −91 avec 113.
Justifions maintenant cette méthode en envisageant quatre cas lorsqu’on additionne deux entiers relatifs a et b.
Nous allons supposer que a + b est représentable en machine, c’est-à-dire que a + b ∈ ~−2n−1 , 2n−1 − 1) :
– si a > 0 et b > 0 on calcule (a + b) ;
– si a > 0 et b < 0 on calcule a + (2n + b) = (a + b) + 2n ;
– si a + b > 0 ce nombre s’écrit sur n + 1 bit donc est tronqué : on obtient la représentation de a + b ;
– si a + b < 0 ce nombre n’est pas tronqué : on obtient la représentation de a + b ;
– si a < 0 et b > 0 la situation est identique au cas précédent ;
– si a < 0 et b < 0 on calcule (2n + a) + (2n + b) = 2n + (2n + a + b) ; ce nombre s’écrit sur n + 1 bits donc est
tronqué : il reste 2n + a + b qui est la représentation de a + b.
Jean-Pierre Becirspahic
4.6 informatique commune
– si x > 0 il est représenté par l’entier naturel x et son opposé −x est représenté par l’entier naturel 2n − x.
Notons x = (0xn−2 · · · x1 x0 )2 et posons y = (1yn−2 · · · y1 y0 )2 en convenant que yi = 1 − xi . Alors x + y =
(11 · · · 11)2 = 2n − 1 donc 2n − x = y + 1.
– si x < 0 il est représenté par l’entier naturel 2n + x et son opposé −x est représenté par l’entier naturel −x.
Notons 2n +x = (1xn−2 · · · x1 x0 )2 et posons y = (0yn−2 · · · y1 y0 )2 en convenant que yi = 1−xi . Alors 2n +x +y =
(11 · · · 11)2 = 2n − 1 donc −x = y + 1.
Dans les deux cas, nous constatons que pour obtenir la représentation de l’opposé de x il suffit de procéder
ainsi :
(i) considérer la représentation de x et remplacer tous les bits égaux à 0 par des 1 et réciproquement ;
(ii) additionner 1 au résultat obtenu.
Exemples. Illustrons cette démarche avec un codage sur 8 bits.
x = 113 est représenté par 01110001 ; on a donc y = 10001110 et −x est représenté par y + 1 = 10001111.
En effet, 256 − 113 = 143 = (10001111)2 .
x = −91 est représenté par 10100101 ; on a donc y = 01011010 et −x est représenté par y + 1 = 01011011.
En effet, 91 = (1011011)2 .
Dépassement de capacité
Nous venons de voir que si un processeur alloue n bits à la représentation des entiers relatifs, les entiers
représentables appartiennent à l’intervalle ~−2n−1 , 2n−1 − 1. Or si on effectue des opérations sur ces entiers il
est fort possible que le résultat du calcul n’appartienne plus à cet intervalle.
Observons ce qui se passe lorsqu’une addition dépasse la borne supérieure en considérant l’addition de 72 et de
55 avec un codage sur 8 bits :
72 = (1001000)2 est représenté par 01001000. 59 = (111011)2 est représenté par 00111011.
Effectuons l’addition de ces deux représentations :
0 1 0 0 1 0 0 0
+ 0 0 1 1 1 0 1 1
1 0 0 0 0 0 1 1
On obtient le nombre négatif représenté par 10000011 c’est à dire −125, alors que 72 + 59 = 131. Ce n’est pas
anormal puisque sur 8 bits les seuls entiers relatifs représentables sont compris entre −128 et +127, ce qui n’est
pas le cas de 131.
On peut observer que le résultat obtenu, −125, est égal à 131 − 256 ; ce n’est pas un hasard et ceci résulte de la
propriété suivante :
Théorème. — Dans une représentation en complément à deux sur n bits, le successeur de 2n−1 − 1 est égal à −2n−1 .
Preuve. Ceci est immédiat si on se souvient que 2n−1 − 1 est représenté par 0111......1111 et −2n−1 par
1000......0000.
Autrement dit, les entiers relatifs représentés par complémentation à deux sont pourvus d’une structure
cyclique, que l’on peut représenter par le schéma ci-dessous, les flèches désignant le successeur d’un entier :
−1
1
···
···
−2n−1
2n−1 − 1
Représentation des nombres 4.7
Certains langages de programmation déterminent un éventuel dépassement de capacité mais d’autres, pour des
raisons d’efficacité 4 , ne le font pas. C’est par exemple les cas du langage Caml utilisé par l’option informatique :
# max_int ;;
− : i n t = 4611686018427387903
# min_int ;;
− : i n t = −4611686018427387904
# max_int + 1 ;;
− : i n t = −4611686018427387904
En Caml les entiers sont codés en complément à deux sur 63 bits. Nous avons :
In [3]: x
Out[3]: 9223372036854775807
In [4]: x + 1
Out[4]: 9223372036854775808
Que s’est-il passé ? Contrairement au langage Caml, l’interprète Python détecte le débordement de capacité :
lorsque le résultat d’un calcul dépasse le plus grand entier représentable en complément à deux (à savoir 263 − 1)
la représentation des entiers change pour adopter la représentation sous forme dite des entiers longs.
Contrairement au complément à deux, cette représentation n’est pas limitée en taille. Cela présente bien
évidemment des avantages, mais aussi des inconvénients : les opérations sur les entiers longs prennent plus de
temps que sur les entiers classiques. Nous n’aborderons pas le problème de la représentation interne des entiers
longs, plus complexe que la représentation par complément à deux.
Jean-Pierre Becirspahic
4.8 informatique commune
binaire/décimal est exacte, mais cela pose un problème pour les nombres décimaux car le développement dyadique
d’un nombre décimal peut être infini 5 .
Par exemple, le nombre 0, 1 a une représentation décimale finie et une représentation dyadique infinie :
0, 1 = (0, 000110011001100110011001100110011001100110011 · · · )2
Ainsi, sa représentation machine sera nécessairement tronquée et ne correspondra pas exactement au nombre
0, 1 (mais en sera néanmoins très proche).
La conversion d’un nombre décimal en nombre dyadique va donc souvent provoquer une approximation qui
dans certains cas conduit à des résultats qui peuvent paraître étranges à un utilisateur non averti. Par exemple :
In [1]: 0.1 + 0.2
Out[1]: 0.30000000000000004
De ceci il faudra retenir qu’un calcul sur des nombres décimaux sera toujours entaché d’une certaine marge
d’erreur dont il faudra tenir compte, avec une conséquence importante :
L’égalité entre nombres flottants n’a pour ainsi dire aucun sens.
Quand on utilise le type float, il est rarissime (hormis dans un but pédagogique) d’utiliser l’égalité de valeur == ;
on fera toujours intervenir une marge d’erreur (absolue ou relative).
In [4]: abs(0.1 + 0.2 − 0.3) <= 1e−10
Out[4]: True
(On a choisi ici une marge d’erreur absolue en considérant que toute quantité inférieure ou égale à 10−10 est
nulle.)
La norme IEEE-754
Cette norme est actuellement le standard pour la représentation des nombres à virgule flottante en binaire.
Nous allons en donner une description (incomplète) pour une architecture 64 bits.
Un nombre dyadique non nul possède une représentation normalisée de la forme ±(1, b1 · · · bk )2 × 2e , où e est
un entier relatif. Par exemple, 6, 25 = (110, 01)2 a pour représentation normalisée (1, 1001)2 × 22 et −0, 375 =
−(0, 011)2 la représentation normalisée −(1, 1)2 × 2−2 .
La suite de bits b1 · · · bk est appelée la mantisse du nombre, et la puissance de 2, l’exposant.
Dans cette norme, les nombres dyadiques sont codés sur 64 bits en réservant :
– 1 bit pour le signe ;
– 11 bits pour l’exposant ;
– 52 bits pour la mantisse.
s e m
11 bits 52 bits
L’exposant est un entier relatif, mais pour permettre une comparaison plus aisée des nombres flottants, il n’est
pas codé suivant la technique de complément à deux mais suivant la technique du décalage : l’exposant e est
représenté en machine par l’entier positif e0 = e + 210 − 1.
Un entier naturel codé sur 11 bits est compris entre 0 et 211 − 1 donc a priori :
Cependant, les valeurs extrêmes (e0 = 0 et e0 = 211 − 1) sont réservées à la représentation de certaines valeurs
particulières, comme nous allons le voir maintenant.
5. En revanche, la conversion réciproque ne pose pas de problème, puisque tout nombre dyadique est aussi décimal.
Représentation des nombres 4.9
et le plus grand :
(1, 1111 · · · 1111)2 × 21023 = 21024 − 2971 ≈ 1, 80 × 10308
| {z }
52 fois 1
– si e0 = (00000000000)2 , autrement dit si e = 1 − 210 = −1023, le nombre est représenté sous la forme dénormali-
sée ±(0, b1 · · · bk )2 × 2e . Cette optimisation permet une meilleure représentation des nombres au voisinage de 0
(mais nous ne rentrerons pas dans ces détails, très techniques). Le plus petit nombre dénormalisé strictement
positif est :
(0, 0000 · · · 0000 1)2 × 2−1023 = 2−1074 ≈ 4, 94 × 10−324
| {z }
51 fois 0
et le plus grand :
(0, 1111 · · · 1111)2 × 2−1022 = 2−1022 − 2−1074 ≈ 2, 23 × 10−308
| {z }
52 fois 1
Parmi ces nombres dénormalisés figure 0, représenté par une mantisse nulle (et un bit de signe quelconque).
– enfin, e0 = (11111111111)2 permet de coder des valeurs particulières : l’infini qu’on obtient lorsqu’un calcul
dépasse le plus grand nombre représentable (mantisse nulle), et le NaN ("Not a Number") qu’on obtient
comme résultat d’une opération invalide (mantisse non nulle).
Remarque. Ces deux valeurs et en particulier le nombre flottant inf, supportent les opérations et comparaisons
usuelles :
∀x ∈ R, x + inf = inf et ∀x ∈ R, x < inf
Leur usage peut permettre de simplifier certains algorithmes ; nous aurons l’occasion d’en reparler.
In [3]: a = float('inf') # le moyen le plus simple pour définir inf
In [4]: a + 5
Out[4]: inf
In [5]: a * (−2)
Out[5]: −inf
In [6]: a − a
Out[6]: nan
Exercice 3 Dans le type float16 de Numpy les nombres flottants sont représentés sur 16 bits : 1 bit pour le
signe, 5 bits pour l’exposant, 10 bits pour la mantisse.
Donner la représentation machine dans ce type de 1, de −2, puis du plus petit nombre strictement supérieur à 1
(ainsi que sa valeur).
Donner les représentations machine et la valeur des plus petits et plus grands nombres positifs normalisés.
Déterminer quel nombre est représenté par : 0|01110|1001001000.
Jean-Pierre Becirspahic
4.10 informatique commune
Aucun des trois nombres 0, 1, 0, 2 et 0, 3 n’est un nombre dyadique, donc leurs représentations machine n’est
qu’approximative.
Évidemment, un calcul isolé ne produira pas d’erreur importante, mais il peut en être autrement lorsqu’une
longue suite de calculs provoque un cumul des erreurs d’arrondi (le fameux « effet papillon » dans l’étude de
phénomènes chaotiques).
Avant de poursuivre, nous allons tenter de décortiquer le calcul ci-dessus.
Lors de la conversion décimal −→ dyadique la règle est d’arrondir au plus proche 6 . Par exemple, la repré-
sentation dyadique de 0, 1 est infinie : 0, 1 = (0, 00011001 · · · )2 (le motif 1001 se répète infiniment) donc la
représentation machine de 0, 1 est :
Pour additionner ces deux quantités, la plus petite des deux voit ses bits décalés d’un cran : 0, 1 est provisoire-
ment représenté par 0, 1 1001 1001 · · · 1001 101 × 2−3 = 0, 1100 1100 · · · 1100 1101 × 2−3 .
| {z }
48 bits
On réalise l’addition de ces deux nombres dyadiques pour obtenir :
Ce résultat est enfin normalisé (donc arrondi au plus proche), ce qui donne : 1, 0011 0011 · · · 0011 0100 × 2−2 .
| {z }
48 bits
Sachant que 0, 3 possède la représentation dyadique infinie : (0, 010011 · · · )2 sa représentation machine est
1, 0011 0011 · · · 0011 0011 × 2−2 , ce qui explique que le calcul 0.1 + 0.2 − 0.3 ne donne pas 0 mais le nombre
| {z }
48 bits
0, 0000 0000 · · · 0000 0001 × 2−2 = 2−54 . Vérifions-le :
| {z }
48 bits
In [6]: 2**(−54)
Out[6]: 5.551115123125783e−17
Absorption
En mathématique, l’addition est associative : (x + y) + z = x + (y + z) ; ce n’est pas le cas pour l’addition entre
nombres flottants :
In [7]: (1. + 2.**53) − 2.**53
Out[7]: 0.0
6. Plus précisément, la règle est : round to nearest, ties to even. En cas d’égalité, le nombre pair le plus proche est choisi.
Représentation des nombres 4.11
Le résultat de ce calcul est facile à comprendre. L’écriture normalisée de 1 + 253 est égale à :
(1, 0000 · · · 0000 1)2 × 253
| {z }
52 fois 0
mais comme la mantisse n’occupe que 52 bits, le résultat de cette addition n’est pas représentable en machine et
sera approché par :
(1, 0000 · · · 0000)2 × 253
| {z }
52 fois 0
53 53
Autrement dit, en arithmétique flottante, 1 + 2 = 2 . Ce mécanisme porte le nom d’absorption : l’addition de
deux quantités dont l’écart relatif est très important entraîne l’absorption de la plus petite de ces deux quantités par la
plus grande.
En conséquence, il est parfois nécessaire de réorganiser les calculs pour obtenir un résultat plus conforme à nos
attentes :
In [8]: 1. + (2.**53 − 2.**53)
Out[8]: 1.0
Cancellation
Un autre problème se présente lors de la soustraction de deux quantités très proches. Pour fixer les idées,
supposons que l’on connaisse deux quantités voisines x et y avec une précision de 20 chiffres significatifs après
la virgule :
x = 1,10010010000111111011
y = 1,10010010000111100110
x−y = 0,00000000000000010101
le résultat x − y sera normalisé pour être représenté en machine par (1, 0101)2 × 2−16 ; autrement dit, la précision
ne sera plus que de 4 chiffres après la virgule. Cette perte drastique de précision porte le nom de cancellation,
car la différence de deux quantités très voisines fait littéralement s’évanouir les chiffres significatifs.
Prendre conscience de ce phénomène permet de conditionner un calcul pour le rendre moins sensible à
l’évanescence des chiffres significatifs. Par exemple, pour un calcul numérique il est préférable de calculer
1 1 1
plutôt que − car, bien que ces deux quantités soient mathématiquement égales, la seconde est
x(x + 1) x x+1
beaucoup plus sensible au phénomène de cancellation.
En règle générale, lors d’un calcul numérique il faut suivre les recommandations suivantes :
– on évite d’additionner deux quantités dont l’écart relatif est très grand ;
– on évite de soustraire deux quantités très voisines.
Et pour finir. . .
Il ne faudrait surtout pas croire que ces petites erreurs de calcul et d’arrondi soient négligeables, et l’anecdote
suivante devrait vous convaincre de l’importance qu’il y a à en prendre conscience.
Le 25 février 1991, à Dharan en Arabie Saoudite, un missile Patriot américain a raté l’interception d’un missile
Scud irakien, ce dernier provoquant la mort de 28 personnes. La commission d’enquête chargée de comprendre
la raison de cet échec a mis en évidence le défaut suivant :
L’horloge interne du missile Patriot mesure le temps en 1/10s. Pour obtenir le temps en seconde, le système
multiplie ce nombre par 10 en utilisant un registre de 24 bits en virgule fixe. Or 1/10 n’est pas un nombre
dyadique donc a été arrondi : le registre de 24 bits contient (0, 00011001100110011001100)2 et induit une erreur
binaire de (0, 0000000000000000000000011001100 . . . )2 , soit approximativement 0, 000000095 en notation
décimale.
En multipliant cette quantité par le nombre de 1/10s pendant 100h (le temps écoulé entre la mise en marche
du système et le lancement du missile Patriot), on obtient le décalage entre l’horloge interne de missile et le
temps réel, soit :
0, 000000095 × 100 × 3600 × 10 ≈ 0, 34s.
Or un missile Scud vole à la vitesse approximative de 1, 676m/s donc parcourt plus de 500m en 0, 34s, ce qui le
fait largement sortir de la zone d’acquisition de sa cible par le missile d’interception 7 .
7. Référence : http://ta.twi.tudelft.nl/nw/users/vuik/wi211/disasters.html.
Jean-Pierre Becirspahic
informatique commune
Chapitre 5
Listes et séquences
Une structure de données est une façon de ranger et d’ordonner des objets. Il en existe de plusieurs types, qui se
distinguent par la façon d’accéder aux éléments de la structure de données et par la façon de modifier cette
dernière (en ajoutant ou en ôtant des éléments).
La principale structure de données en Python est assurément la classe list. Il s’agit d’une structure hybride, qui
cherche à tirer avantage de deux structures de données fondamentales en informatique : les tableaux et les listes
chaînées, qui n’existent pas en tant que telles en Python. Nous allons commencer par décrire les caractéristiques
principales de ces deux structures, avant d’observer en quoi la classe list s’en s’inspire.
1.1 Tableaux
Les tableaux forment une suite de variables de même type associées à des emplacements consécutifs de la
mémoire.
d
a
Puisque tous les emplacements sont de même type, ils occupent tous le même nombre d de cases mémoire ;
connaissant l’adresse a de la première case du tableau, on accède en coût constant à l’adresse de la case d’indice
k en calculant a + kd. En revanche, ce type de structure est statique : une fois un tableau créé, la taille de ce
dernier ne peut plus être modifiée faute de pouvoir garantir qu’il y a encore un espace mémoire disponible au
delà de la dernière case. En résumé :
– un tableau est une structure de donnée statique ;
– les éléments du tableau sont accessibles en lecture et en écriture en temps constant.
Remarque. Le module Numpy propose une implémentation des tableaux que nous utiliserons au second
semestre : la classe array.
Jean-Pierre Becirspahic
5.2 informatique commune
a nil
Dans une liste chaînée, il est impossible de connaître à l’avance l’adresse d’une case en particulier, à l’exception
de la première. Pour accéder à la ne case il faut donc parcourir les n − 1 précédentes : le coût de l’accès à une
case est proportionnelle à la distance qui la sépare de la tête de la liste. En contrepartie, ce type de structure
est dynamique : une fois la liste crée, il est toujours possible de modifier un pointeur pour insérer une case
supplémentaire. En résumé :
– une liste chaînée est une structure de donnée dynamique ;
– le ne élément d’une liste chaînée est accessible en temps proportionnel à n.
est une liste qui comporte 6 éléments. Comme on peut le constater, une liste peut contenir une collection
hétérogène d’objets (des entiers, des flottants, des chaînes de caractères. . .).
Notons déjà qu’une liste étant elle-même un objet python, il est tout à fait possible qu’une liste contienne des
listes parmi ses éléments :
b = [[], [1], [1,2], [1, 2, 3]]
On accède à chaque élément individuel de la liste par l’intermédiaire de son index. Attention, le premier
élément de la liste possède l’index 0 (tout comme les chaînes de caractères). Pour connaître l’index du dernier
élément, on peut calculer la longueur d’une liste à l’aide de la fonction len ; l’index du dernier élément d’une
liste nommée l est donc len(l) − 1.
In [1]: a[2]
Out[1]: 'abc'
In [2]: b[3][1]
Out[2]: 2
In [3]: len(b)
Out[3]: 4
Listes et séquences 5.3
Lorsque l’index est négatif, le décompte est pris en partant de la fin ; ainsi le dernier élément porte l’index −1,
l’avant-dernier l’index −2, etc 1 .
In [4]: a[−2]
Out[4]: 'de'
En d’autres termes, si k est un entier non nul, l’élément d’indice −k coïncide avec l’élément d’indice ` − k, où `
désigne la longueur de la liste.
2.2 Slicing
À l’instar des chaînes de caractères, python permet d’effectuer des coupes (le slicing) : si l est une liste, alors
l[i:j] crée une nouvelle liste constituée des éléments dont les index sont compris entre i et j − 1 :
In [5]: a[1:5]
Out[5]: [1, 'abc', 4.5, 'de']
In [6]: a[1:−1]
Out[6]: [1, 'abc', 4.5, 'de']
Lorsque l’index i est absent, il est pris par défaut égal à 0 ; lorsque j est absent, il est pris par défaut égal à la
longueur de la liste.
In [7]: a[:4]
Out[7]: [0, 1, 'abc', 4.5]
In [8]: a[−3:]
Out[8]: [4.5, 'de', 6]
On peut observer sur les exemples ci-dessus que l[:n] permet d’obtenir les n premiers éléments de la liste et
l[−n:] les n derniers.
Sélection partielle
Si nécessaire, la syntaxe l[debut:fin] possède un troisième paramètre (égal par défaut à 1) indiquant le pas de
la sélection. La commande l[debut:fin:pas] donne tous les éléments de la liste dont les index sont compris
entre debut (au sens large) et fin (au sens strict) et espacés d’un pas égal à pas. Par exemple :
In [9]: l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
In [10]: l[2:9:3]
Out[10]: [2, 5, 8]
In [11]: l[3::2]
Out[11]: [3, 5, 7, 9]
In [12]: l[::2]
Out[12]: [0, 2, 4, 6, 8]
Il est même possible de choisir un pas négatif, auquel cas la liste est parcourue à rebours. En particulier,
l[::−1] retourne une nouvelle liste dont l’ordre des éléments est inversé 2 :
In [13]: l[::−1]
Out[13]: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
1. Les conventions d’indexation sont identiques à celles des chaînes de caractères (cf. chapitre 2).
2. On observera que pour un pas négatif les valeurs par défaut de début et de fin sont inversées.
3. Rappelons que range(i, j, p) énumère les entiers compris entre i (au sens large) et j (au sens strict) espacés d’un pas égal à p, les
paramètres i et p étant par défaut pris égaux respectivement à 0 et 1.
Jean-Pierre Becirspahic
5.4 informatique commune
In [14]: list(range(11))
Out[14]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
La conversion est aussi possible entre une chaîne de caractères et une liste :
In [16]: list("Louis−Le−Grand")
Out[16]: ['L', 'o', 'u', 'i', 's', '−', 'L', 'e', '−', 'G', 'r', 'a', 'n', 'd']
Cependant, la conversion de type est toujours un peu dangereuse, faute de connaître précisément la manière
dont la conversion s’opère. Aussi, il est souvent préférable de définir une liste par filtrage du contenu d’une énu-
mération
n selon un principe
o analogue à une définition mathématique. Par exemple, à la définition mathématique
x ∈ ~0, 10 x2 6 50 correspond la définition par compréhension suivante :
Ceci nous donne un moyen simple de calculer le produit cartésien de deux listes :
In [18]: a, b = [1, 3, 5], [2, 4, 6]
Autre exemple plus élaboré, on appelle triplet pythagoricien tout triplet d’entiers (x, y, z) tels que x2 + y 2 = z 2 . La
définition par compréhension nous donne un moyen élégant de les calculer :
In [20]: [(x, y, z) for x in range(1, 20) for y in range(x, 20) for z in range(y, 20)
if x * x + y * y == z * z]
Out[20]: [(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), (9, 12, 15)]
Remarque. Bien que les listes définies par compréhension ressemblent beaucoup aux ensembles mathéma-
tiques, elles n’en restent pas moins différentes par le fait qu’elles sont ordonnées. Ce n’est pas non plus forcément
la manière la plus efficace de procéder. Dans l’exemple précédent tous les triplets d’entiers vérifiant les rela-
tions 1 6 x 6 y 6 z < 20 sont énumérés puis testés un par un. Cette démarche peut s’avérer très longue si on
(n + 1)n(n − 1)
remplace 20 par un entier n plus grand : le nombre de triplets énumérés est égal à donc croît
6
proportionnellement (ou presque) avec le cube de n. Pour n = 1000 le nombre de triplets à envisager est de
l’ordre de 166 millions !
In [3]: l
Out[3]: [0, 1, 2, 'a', 4, 5, 6, 7, 8, 9, 10]
In [5]: l
Out[5]: [0, 1, 2, 'a', 'b', 'c', 7, 8, 9, 10]
Il est même possible d’employer la syntaxe complète du slicing en introduisant un pas (mais attention, dans ce
cas les nouveaux éléments doivent être en même nombre que ceux qu’ils remplacent). À réserver aux utilisateurs
avertis !
In [6]: l = list(range(10))
In [8]: l
Out[8]: [0, 0, 2, 2, 4, 4, 6, 6, 8, 8]
In [11]: l
Out[11]: [0, 1, 2, 6, 7, 8, 9, 10]
4. Une séquence est le plus souvent une liste ou une chaîne de caractères. Voir la section suivante.
5. La programmation objet ne sera détaillée qu’en seconde année.
Jean-Pierre Becirspahic
5.6 informatique commune
In [13]: l.append('e')
In [14]: l
Out[14]: ['a', 'b', 'c', 'd', 'e']
In [16]: l
Out[16]: ['a', 'b', 'x', 'c', 'd', 'e']
In [17]: l = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
In [18]: l.remove(4)
In [19]: l
Out[19]: [1, 2, 3, 5, 1, 2, 3, 4, 5]
In [20]: l.pop(−1)
Out[20]: 5
In [21]: l
Out[21]: [1, 2, 3, 5, 1, 2, 3, 4]
On notera que les deux méthodes remove et pop modifient la liste l, mais pop retourne la valeur supprimée,
alors que remove retourne la valeur None.
In [22]: l = [1, 2, 3, 4, 1, 2, 3, 4]
In [23]: l.reverse()
In [24]: l
Out[24]: [4, 3, 2, 1, 4, 3, 2, 1]
In [25]: l.sort()
In [26]: l
Out[26]: [1, 1, 2, 2, 3, 3, 4, 4]
Remarque. On peut observer que les méthodes définies ci-dessus modifient l’objet à qui elles s’appliquent, à la
différence d’une fonction qui en général calcule un nouvel objet. Il existe par exemple une fonction sorted qui
calcule une nouvelle liste comportant les mêmes éléments mais cette fois-ci triés.
In [27]: l = [1, 2, 3, 4, 1, 2, 3, 4]
In [28]: sorted(l)
Out[28]: [1, 1, 2, 2, 3, 3, 4, 4]
In [29]: l
Out[29]: [1, 2, 3, 4, 1, 2, 3, 4]
Comme on peut le constater avec l’exemple ci-dessus, la fonction sorted retourne un résultat (contrairement à
la méthode sort) mais ne modifie pas la liste l.
Listes et séquences 5.7
In [1]: list('abcde')
Out[1]: ['a', 'b', 'c', 'd', 'e']
Mutabilité
Contrairement aux listes, les chaînes de caractères et les tuples ne sont pas mutables : on ne peut modifier ou
supprimer les éléments d’une chaîne de caractères ou d’un tuple. C’est là une différence importante, qui va
souvent conditionner le choix d’une structure de données (liste ou tuple) plutôt qu’une autre en fonction de ce
qu’on souhaite faire.
Ainsi, pour modifier un tuple, on a pas d’autre possibilité que de le recréer entièrement.
Pour s’en convaincre, nous allons utiliser la fonction id qui, rappelons-le, renvoie l’adresse mémoire où est
stocké l’objet à qui on l’applique. Nous savons que lorsqu’on crée une variable, c’est-à-dire lorsqu’on lie un
identificateur à un objet par l’instruction nom = obj, on associe en fait à cet identificateur l’adresse mémoire
où se trouve stocké cet objet. Ainsi, les instructions l1 = [1, 2, 3] et tup1 = (1, 2, 3) créent des liens
symboliques (des pointeurs) entre les noms choisis et des emplacement en mémoire contenant les valeurs
données :
In [3]: l1 = [1, 2, 3]
In [4]: id(l1)
Out[4]: 4448440520
l1 [1, 2, 3] tup1 (1, 2, 3)
In [5]: tup1 = (1, 2, 3)
In [6]: id(tup1)
Out[6]: 4448266352
Une liste étant mutable, on peut la modifier sans pour autant créer une nouvelle référence vers l’objet : l’adresse
mémoire reste la même. L’instruction l1[0] = 4 modifie le contenu de l’emplacement mémoire référencé
par le nom l1 mais pas l’adresse elle-même. En revanche, l’instruction tup1 = (4, 2, 3) recrée un nouveau
référencement à une adresse différente.
Jean-Pierre Becirspahic
5.8 informatique commune
In [7]: l1[0] = 4
In [8]: id(l1)
Out[8]: 4448440520 l1 [4, 2, 3] tup1 (1, 2, 3)
Il est important de bien comprendre le mécanisme mis en œuvre car s’agissant de structures linéaires la recopie
d’une liste ou d’un tuple à un autre emplacement de la mémoire risque fort de nécessiter un temps d’exécution
proportionnel à la longueur de la liste ou du tuple 6 .
On peut maintenant préciser la définition d’un objet mutable :
Définition. — Un objet Python est dit mutable lorsqu’on peut le modifier sans changer son adresse en mémoire.
Pour l’instant, les seules structures mutables que l’on connaisse sont les listes.
In [11]: l2 = l1
Ce n’est pas un problème lorsque la variable n’est pas mutable. Si on veut revenir à l’état initial de tup1 il faut
de toute façon le recréer entièrement en écrivant : tup1 = (1, 2, 3). En revanche, l’instruction l1[0] = 1 va
aussi modifier la variable l2 puisque ces deux noms référencent la même adresse.
In [15]: l1[0] = 1
In [16]: l2
Out[16]: [1, 2, 3] l1 [1, 2, 3] tup1 (1, 2, 3)
Retenons donc que si l1 est une liste, l’instruction l2 = l1 référence sous un autre nom le même objet tandis
que l’instruction l3 = l1[:] crée une nouvelle copie de la liste.
Remarque. De manière équivalente à l3 = l1[:] on peut écrire l3 = l1.copy() dont la syntaxe est peut-être
plus explicite.
Copie profonde
Considérons maintenant le cas d’une liste dont les éléments sont eux-mêmes des listes (ou plus généralement des
objets mutables) et créons-en une copie.
Nous constatons sur cet exemple que nous n’avons réalisé qu’une copie de premier niveau : l1 et l2 référencent
effectivement deux emplacements mémoires distincts, mais l1[i] et l2[i] référencent toujours le même objet
mutable et toute modification de l’un entrainera automatiquement une modification identique de l’autre.
Il est possible de s’en sortir avec une définition par compréhension :
l3 = [l[:] for l in l1]
mais outre le fait que la syntaxe utilisée se fait de moins en moins explicite, on ne résout pas le problème s’il y a
un troisième niveau d’objets mutables, voire plus. . .
Dans ces situations (et même s’il faut bien reconnaître qu’il est très rare de dépasser trois niveaux d’objets
mutables) on utilise un module spécialisé dans la copie : le module copy et plus précisément la fonction
deepcopy de ce module qui réalise une copie profonde d’un objet mutable :
for x in seq
où seq est une séquence (donc une liste, une chaîne de caractères ou un tuple). Une telle boucle définit une
variable (ici nommée x) qui prend successivement toute les valeurs de la séquence (par ordre d’indice croissant) ;
cela en fait le mode privilégié de parcours complet d’une liste.
Jean-Pierre Becirspahic
5.10 informatique commune
Supposons par exemple que l’on veuille calculer la somme des éléments d’une liste d’entier. On définira la
fonction :
def somme(l):
s = 0
for x in l:
s += x
return s
• Calcul de la moyenne
n
1X
Rappelons que la moyenne d’une suite de valeurs (x1 , x2 , . . . , xn ) se définit par : x = xk .
n
k=1
def moyenne(l):
s = 0
for x in l:
s += x
return s / len(l)
• Calcul de la variance
n
1X
Rappelons que la variance d’une suite finie de valeurs (x1 , x2 , . . . , xn ) se définit par : v = (xk − x)2 et ca-
n
k=1
ractérise la dispersion d’un échantillon autour de sa moyenne. Cependant cette formule exige deux parcours
de la liste : le premier pour calculer x et le second pour calculer v. Il est préférable d’utiliser la formule de
König-Huygens :
n n n n n n 2
1X 1 X 2 2x X 1X 2 1X 2 1
X
(xk − x)2 = xk − xk + x 2 = xk − x 2 = xk − xk
n n n n n n
k=1 k=1 k=1 k=1 k=1 k=1
def variance(l):
n = len(l)
s = c = 0
for x in l:
s += x
c += x * x
return c/n − (s/n) * (s/n)
• Calcul du maximum
Intéressons nous maintenant au problème du calcul de la valeur maximale d’une liste d’entiers :
def maximum(l):
m = l[0]
for x in l:
if x > m:
m = x
return m
Notons qu’il existe déjà une fonction max (ainsi qu’une fonction min) qui calcule le maximum d’une séquence
d’entiers.
Si on souhaite obtenir l’indice de l’élément maximal de la liste, il faut cette fois parcourir non plus la liste
elle-même, mais la liste de ces indices :
Listes et séquences 5.11
def indice_du_max(l):
i, m = 0, l[0]
for k in range (1, len(l)):
if l[k] > m:
i, m = k, l[k]
return i
On peut constater que le code de la fonction ci-dessus s’est alourdi et va à l’encontre du principe des itéra-
teurs en Python. C’est pourquoi le langage propose une fonction enumerate qui renvoie une liste de tuples
(indice, valeur) lorsqu’on l’applique à une liste (ou plus généralement à tout objet énumérable). Au code
précédent on préférera donc :
def indice_du_max(l):
i, m = 0, l[0]
for (k, x) in enumerate(l):
if x > m:
i, m = k, x
return i
Le code produit est assez lourd et n’est guère dans l’esprit de la programmation en Python.
La seconde façon, beaucoup plus dans l’esprit des itérateurs Python, consiste à interrompre une boucle
énumérée dès qu’on a trouvé l’élément recherché :
Cette dernière version est déjà particulièrement simple, mais on peut faire encore plus court en utilisant
l’instruction in. Ce mot-clé, qu’on utilise déjà dans les boucles énumérées, a un second rôle : celui justement
de déterminer la présence ou non d’un élément dans une séquence. Les deux fonctions ci-dessus sont donc
équivalentes à la simple expression :
x in l
Jean-Pierre Becirspahic
5.12 informatique commune
• Recherche dichotomique
Les trois fonctions précédentes ont en commun de nécessiter dans le pire des cas n comparaisons (où n est
la longueur de la liste) pour déterminer la présence ou non de l’élément cherché dans la liste. On peut faire
beaucoup mieux dans le cas d’une liste triée.
Pour chercher x dans la liste triée par ordre croissant [a0 , . . . , an−1 ], nous allons appliquer le principe dichoto-
jnk
mique : comparer x et ap , avec p = .
2
– si x < ap alors x, s’il se trouve dans la liste, ne peut qu’être dans la liste [a0 , . . . , ap−1 ] ;
– si x = ap , la recherche est terminée ;
– si x > ap alors x, s’il se trouve dans la liste, ne peut qu’être dans la liste [ap+1 , . . . , an−1 ].
La mise en œuvre pratique utilise deux variables entières i et j délimitant le domaine [ai , . . . , aj−1 ] dans lequel
on cherche x. Les valeurs initiales de ces variables sont donc i = 0 et j = n.
ji + j k
On compare ensuite x à l’élément médian d’indice k = et si nécessaire on poursuit la recherche en
2
remplaçant j par k ou i par k + 1.
6 ak ak > ak
i k j −1
La recherche s’achève (par une réponse négative) lorsque i = j car le domaine de recherche est vide.
1. Introduction
Déterminer la complexité 1 d’un algorithme, c’est évaluer les ressources nécessaires à son exécution (essentielle-
ment la quantité de mémoire requise) et le temps de calcul à prévoir. Ces deux notions dépendent de nombreux
paramètres matériels qui sortent du domaine de l’algorithmique : nous ne pouvons attribuer une valeur absolue
ni à la quantité de mémoire requise ni au temps d’exécution d’un algorithme donné. En revanche, il est souvent
possible d’évaluer l’ordre de grandeur de ces deux quantités de manière à identifier l’algorithme le plus efficace
au sein d’un ensemble d’algorithmes résolvant le même problème.
Prenons un exemple concret : la détermination du nombre de diviseurs d’un entier naturel n. Une première
solution consiste à essayer si chacun des entiers compris entre 1 et n est un diviseur de n. Ceci conduit à définir
la fonction diviseurs1 de la figure 1.
Jean-Pierre Becirspahic
6.2 informatique commune
3. Voir le chapitre 4.
Notion de complexité algorithmique 6.3
Cette notation indique que dans le pire des cas, la croissance de f (n) ne dépassera pas celle de la suite (αn ).
L’usage de cette notation exprime l’objectif qu’on se donne le plus souvent : déterminer le temps d’exécution
dans le cas le plus défavorable. On notera qu’un usage abusif est souvent fait de cette notation, en sous-
entendant qu’il existe des configurations de l’entrée pour lesquelles f (n) est√effectivement proportionnel à αn .
On dira par exemple que la complexité de la fonction diviseurs2 est un√O( n) mais jamais qu’elle est un O(n),
même si mathématiquement cette assertion est vraie puisque f (n) = O( n) =⇒ f (n) = O(n).
D’un usage beaucoup moins fréquent, la notation Ω exprime une minoration du meilleur des cas :
L’expérience montre cependant que pour de nombreux algorithmes le cas « moyen » est beaucoup plus souvent
proche du cas le plus défavorable que du cas le plus favorable. En outre, on souhaite en général avoir la certitude
de voir s’exécuter un algorithme en un temps raisonnable, ce que ne peut exprimer cette notation.
Enfin, lorsque le pire et le meilleur des cas ont même ordre de grandeur, on utilise la notation Θ :
f (n) = Θ(αn ) ⇐⇒ f (n) = O(αn ) et f (n) = Ω(αn ) ⇐⇒ ∃A, B > 0 Aαn 6 f (n) 6 Bαn .
Cette notation exprime le fait que quelle que soit le configuration de l’entrée, le temps d’exécution de l’algo-
rithme sera grosso-modo proportionnel à αn .
log n n n log n n2 n3 2n
102 7 ns 100 ns 0, 7 µs 10 µs 1 ms 4 · 1013 années
103 10 ns 1 µs 10 µs 1 ms 1s 10292 années
104 13 ns 10 µs 133 µs 100 ms 17 s
105 17 ns 100 µs 2 ms 10 s 11, 6 jours
106 20 ns 1 ms 20 ms 17 mn 32 années
La lecture de ce tableau est édifiante : on comprend que les algorithmes ayant une complexité supérieure à une
complexité quadratique soient en général considérées comme inutilisables en pratique (sauf pour de petites
voire très petites valeurs de n).
O(log n) logarithmique
O(n) linéaire
O(n log n) quasi-linéaire
O(n2 ) quadratique
k
O(n ) (k > 2) polynomiale
n
O(k ) (k > 1) exponentielle
Jean-Pierre Becirspahic
6.4 informatique commune
Exercice 1 Pour vous familiariser avec ces notions, évaluez pour chacune des fonctions suivantes le temps
d’exécution en fonction de n :
Pour évoquer la notion de complexité en moyenne, il est nécessaire de disposer d’une hypothèse sur la
distribution des données. Dans le cas de l’algorithme de recherche séquentielle, nous allons supposer que les
éléments du tableau sont des entiers distribués de façon équiprobable entre 1 et k ∈ N∗ (au sens large).
Nous disposons donc de k n tableaux différents ; parmi ceux-ci, (k − 1)n ne contiennent pas l’élément que l’on
cherche et dans ce cas l’algorithme procède exactement à n comparaisons.
Dans le cas contraire, l’entier recherché est dans le tableau, et sa première occurrence est dans la i e case avec la
(k − 1)i−1
probabilité . L’algorithme réalise alors i comparaisons. La complexité moyenne est donc égale à :
ki
n
(k − 1)n X (k − 1)i−1
C(n) = × n + × i.
kn ki
i=1
n
X 1 + (nx − n − 1)xn
À l’aide de la formule : ixi−1 = (valable pour x , 1) cette expression se simplifie en :
(1 − x)2
i=1
1 n n 1 n 1 n
C(n) = n 1 − +k 1− 1+ 1− = k 1− 1− .
k k k k
Lorsque k est petit devant n nous avons C(n) ≈ k et il est légitime de considérer que la complexité moyenne est
constante ; lorsque n est petit devant k, C(n) ≈ n et la complexité moyenne rejoint la complexité dans le pire des
cas.
Jean-Pierre Becirspahic
informatique commune
Chapitre 7
Premiers algorithmes
numériques
Dans ce chapitre, nous allons rencontrer nos premiers algorithmes numériques, c’est à dire des algorithmes qui
travaillent sur des nombres flottants et non plus sur des entiers. Il importe de se souvenir que contrairement
aux calculs sur le type int, les calculs sur le type float comportent de nombreuses approximations : conversion
entre nombres décimaux et nombres dyadiques, calculs approchés. . . avec pour conséquences les phénomènes
d’absorbtion et de cancellation 1 .
1. Se référer au chapitre 4.
Jean-Pierre Becirspahic
7.2 informatique commune
Or dans les deux cas le discriminant de l’équation est nul (vérifiez-le en faisant le calcul à la main). Cependant les
erreurs de calculs inhérentes à la manipulation des nombres flottants conduisent dans le premier cas à obtenir un
discriminant égal à 6, 938893903907228·10−18 = 2−57 et dans le second cas à −6, 938893903907228·10−18 = −257 .
Pour pallier à ce problème, on peut observer que lorsque le discriminant ∆ = b2 − 4ac est très petit devant b2
les deux racines sont quasiment confondues. Une solution consiste donc à remplacer la condition ∆ = 0 par
la condition |∆| b2 , ce qui va se traduire concrètement par |∆| 6 εb2 , la valeur de ε étant choisie petite. En
choisissant par exemple ε = 2−52 on obtient la version présentée figure 2.
Remarque. La valeur ε = 2−52 ≈ 2, 22 · 10−16 n’est pas choisie au hasard, il s’agit de l’epsilon machine, c’est-à-dire
la plus grande erreur relative que peut provoquer l’arrondissement arithmétique en virgule flottante. En
d’autres termes, 1 + ε est la plus petite quantité strictement supérieure à 1 qui soit discernable de 1. En effet, la
Premiers algorithmes numériques 7.3
(rappelons que les nombres flottants sont représentés avec une mantisse à 52 bits).
En d’autres termes, il n’y a aucun intérêt à prendre une erreur relative plus petite, comme on peut le constater :
>>> solve2(0.01, 0.2, 1, epsilon=2**(−53))
deux racines simples −10.000000131708903 et −9.999999868291098
>>> solve2(0.011025, 0.21, 1, epsilon=2**(−53))
pas de solution
et dans la pratique il sera prudent de choisir une valeur de ε plus importante sachant que les erreurs d’approxi-
mation peuvent se cumuler.
c b x
a
a+b
2
Jean-Pierre Becirspahic
7.4 informatique commune
Analyse de l’algorithme
La validité de cet algorithme est assurée par l’invariant : ∀n ∈ N, f (un )f (vn ) 6 0, qui assure pour tout n ∈ N
l’existence d’une racine dans l’intervalle [un , vn ].
Il reste à choisir la condition de terminaison de cet algorithme. Sachant que un est une approximation par défaut
u + vn
d’une racine de f et vn une approximation par excès, on choisit une valeur ε > 0 et on retourne wn = n dès
2
lors que vn − un 6 2ε. De la sorte, on est assuré qu’il existe une racine c de f vérifiant : |wn − c| 6 ε.
wn vn
un c
6ε
6 2ε
Coût de l’algorithme
b−a
Il est facile de prouver que vn − un = (l’intervalle de recherche est divisé par deux à chaque itération) ;
2n
b−a b−a
ainsi un résultat est retourné dès lors que n 6 2ε, c’est à dire : n > log2 − 1.
2 ε
−p
En général, on choisit pour ε une puissance de 10 car si ε = 10 cet algorithme assure p − 1 chiffres significatifs
après la virgule (en faisant bien entendu abstraction des approximations successives). Dans ce cas, l’algorithme
se termine dès lors que n > log2 (b − a) + p log2 (10) − 1 = O(p) ; on peut donc affirmer qu’il s’agit d’un algorithme
de coût linéaire vis-à-vis du nombre de décimales souhaitées après la virgule 2 .
Utilisons cet algorithme pour chercher l’unique racine de la fonction sinus entre 3 et 4 :
>>> from numpy import sin
>>> dicho(sin, 3, 4)
3.141592653589214
√
Obtenons maintenant une approximation de 2 :
>>> def f(x): return x * x − 2
...
>>> dicho(f, 1, 2)
1.4142135623724243
2. Nous étudierons bientôt un algorithme bien plus rapide mais moins robuste, l’algorithme de Newton-Raphson.
Premiers algorithmes numériques 7.5
Dans ce dernier exemple, il a été nécessaire de définir la fonction f : x 7→ x2 − 2 avant d’appliquer l’algorithme.
On aurait pu s’en passer en utilisant une fonction anonyme que l’on peut définir à l’aide du mot-clef lambda :
>>> dicho(lambda x: x * x − 2, 1, 2)
1.4142135623724243
>>> help(bisect)
Parameters
−−−−−−−−
f : function
Python function returning a number. f must be continuous, and
f(a) and f(b) must have opposite signs.
a : number
One end of the bracketing interval [a,b].
b : number
The other end of the bracketing interval [a,b].
xtol : number, optional
The routine converges when a root is known to lie within xtol of the
value return . Should be >= 0.
maxiter : number, optional
if convergence is not achieved in maxiter iterations, and error is
raised. Must be >= 0.
Returns
−−−−−
x0 : float
Zero of f between a and b.
Comme on peut le constater, cette fonction agit peu ou prou de la même façon que la notre. Seul paramètre
supplémentaire : un nombre d’itérations maximal au delà duquel une erreur est déclenchée. Ceci peut en effet
être nécessaire si l’utilisateur de cette fonction choisit une valeur de ε trop faible relativement aux valeurs de a
et b. Dans ce cas, et passé un certain rang, il se peut que les valeurs de u et v soient tellement proches que le
u+v
calcul de retourne la valeur de u ou de v. Dans ce cas, l’algorithme ne se termine plus.
2
Il est facile de modifier notre algorithme pour calquer ce comportement :
Jean-Pierre Becirspahic
7.6 informatique commune
L’instruction raise déclenche une exception (ici l’exception RuntimeError) qui met immédiatement fin à
l’exécution de la fonction. Cette exception s’accompagne d’une chaîne de caractères qui s’affiche à la suite de
l’exception :
où f est une fonction continue sur le segment [u, v] à valeurs dans R, est un problème classique intervenant dans
de nombreux domaines, qu’ils soient scientifiques ou non. Cette évaluation peut cependant s’avérer difficile
voire impossible en pratique car il n’est pas toujours possible de déterminer une primitive de la fonction f ,
même en utilisant les techniques de changement de variable ou d’intégration par parties.
Nous allons nous intéresser à certaines méthodes de quadrature qui consistent à approcher la valeur de
l’intégrale par une somme pondérée finie de valeurs de la fonction f en des points choisis ; en d’autres termes
ces méthodes fournissent une approximation de Iu,v (f ) par la quantité :
p
p
X
Iu,v (f )= αi f (xi )
i=0
les coefficients α0 , . . . , αp étant réels et dépendants de l’entier p et les points x0 , . . . , xp appartenant à [u, v].
Dans la formule ci-dessus les points xi et les coefficients αi sont respectivement appelés nœuds et poids de la
formule de quadrature. L’erreur de quadrature est la quantité :
p
Eu,v (f ) = Iu,v (f ) − Iu,v (f )
et on dira qu’une méthode de quadrature est d’ordre k quand l’erreur commise est nulle lorsque f est un
polynôme de degré inférieur ou égal à k.
consistent à subdiviser l’intervalle [a, b] en n sous-intervalles a = u0 < u1 < · · · < un = b (en général avec un pas
constant) et à appliquer une méthode de quadrature sur chacun des intervalles [ui , ui+1 ]. Dans ce cas, l’erreur
Premiers algorithmes numériques 7.7
b−a
On gardera en mémoire l’expression d’une subdivision de pas régulier : uk = a + k , 0 6 k 6 n.
n
u0 u1 u2 u3 u4 u5 u6
a b
b−a
n
Enfin, les différentes majorations des erreurs commises par les méthodes que nous allons étudier reposent sur
le théorème de Rolle, dont on rappelle ici l’énoncé :
Théorème (Rolle). — Si g : [a, b] → R est continue sur le segment [a, b], dérivable sur l’intervalle ]a, b[ et si
g(a) = g(b) alors il existe c ∈ ]a, b[ tel que g 0 (c) = 0.
y = f (x)
u v
Théorème. — La méthode du rectangle est d’ordre 0, et si f est de classe C 1 , l’erreur de quadrature vérifie :
(v − u)2
|Eu,v (f )| 6 M1
2
où M1 est un majorant de |f 0 | sur l’intervalle [u, v].
Jean-Pierre Becirspahic
7.8 informatique commune
Fixons t ∈ ]u, v] et considérons la fonction g : x 7→ f (x) − f (u) − K(x − u), la valeur de K étant choisie de sorte
que g(t) = 0. La fonction g est de classe C 1 et vérifie g(u) = g(t) = 0 donc d’après le théorème de Rolle il existe
c ∈ ]u, t[ tel que g 0 (c) = 0, ce qui signifie que K = f 0 (c). On a donc f (t) − f (u) = f 0 (c)(t − u), ce qui prouve la
majoration : |f (t) − f (u)| 6 M1 (t − u). On en déduit :
Zv
(v − u)2
|Eu,v (f )| 6 M1 (t − u) dt = M1 .
u 2
b−a b−a
pour approcher chacune des intégrales Iuk ,uk+1 (f ) par (uk+1 − uk )f (uk ) = f a+k .
n n
Autrement dit, cette méthode consiste à approcher I(f ) par :
n−1
b−a X b−a
f a+k .
n n
k=0
y = f (x)
a b
(b − a)2
|En (f )| 6 M1
2n
où M1 est un majorant de |f 0 | sur l’intervalle [a, b].
n−1
X (b − a)2 (b − a)2
Preuve. On a |En (f )| 6 |Euk ,uk+1 (f )| avec |Euk ,uk+1 (f )| 6 M1 2
donc |En (f )| 6 M1 .
2n 2n
k=0
Premiers algorithmes numériques 7.9
y = f (x)
u w v
Théorème. — La méthode du point milieu est une méthode d’ordre 1, et si f est de classe C 2 , l’erreur de quadrature
vérifie :
(v − u)3
|Eu,v (f )| 6 M2
24
où M2 est un majorant de |f 00 | sur l’intervalle [u, v].
(t − w) dt = 0. On obtient ainsi :
u
Z v
Eu,v (f ) = f (t) − f (w) − (t − w)f 0 (w) dt.
u
(x − w)2
Fixons alors t , w et considérons la fonction g : x 7→ f (x) − f (w) − (t − w)f 0 (w) − K , la valeur de K étant
2
choisie de sorte que g(t) = 0.
On a g(w) = g(t) = 0 donc d’après le théorème de Rolle il existe c1 ∈ ]w, t[ tel que g 0 (c1 ) = 0. Mais g 0 (x) =
f 0 (x) − f 0 (w) − K(x − w) donc g 0 (w) = 0 ; on peut donc de nouveau appliquer le théorème de Rolle entre c1 et w
et affirmer l’existence d’un réel c2 ∈ ]w, c1 [ tel que g 00 (c2 ) = 0, égalité qui s’écrit : f 00 (c2 ) − K = 0.
(t − w)2
Ainsi, l’égalité g(t) = 0 peut aussi s’écrire f (t) − f (w) − (t − w)f 0 (w) = f 00 (c2 ) ce qui implique :
2
Zv
(t − w)2 (v − u)3
|Eu,v (f )| 6 M2 dt = M2 .
u 2 24
Jean-Pierre Becirspahic
7.10 informatique commune
n−1
b−a X 1b − a
f a+ k+ .
n 2 n
k=0
y = f (x)
a b
(b − a)3
|En (f )| 6 M2
24n2
n−1
X (b − a)3 (b − a)3
Preuve. On a |En (f )| 6 |Euk ,uk+1 (f )| avec |Euk ,uk+1 (f )| 6 M2 donc |En (f )| 6 M 2 .
24n3 24n2
k=0
Théorème. — La méthode du trapèze est une méthode d’ordre 1, et si f est de classe C 2 , l’erreur de quadrature
vérifie :
(v − u)3
|Eu,v (f )| 6 M2
12
où M2 est un majorant de |f 00 | sur l’intervalle [u, v].
Premiers algorithmes numériques 7.11
y = f (x)
u v
Preuve. Si f est un polynôme de degré inférieur ou égal à 1 alors f˜ = f donc Iu,v (f ) = I1u,v (f ) ; la méthode est
bien d’ordre 1.
Si f est de classe C 2 , l’erreur de quadrature est égale à :
Zv
Eu,v (f ) = (f (t) − f˜(t)) dt.
u
(x − v)(x − u)
Fixons t ∈ ]u, v[ et considérons la fonction g : x 7→ f (x) − f˜(x) − K , la constante K étant choisie de
2
sorte que g(t) = 0.
On a g(u) = g(t) = g(v) = 0 donc d’après le théorème de Rolle il existe c1 ∈ ]u, t[ et c2 ∈ ]t, v[ tels que g 0 (c1 ) =
g 0 (c2 ) = 0. Toujours d’après le théorème de Rolle, il existe c3 ∈ ]c1 , c2 [ tel que g 00 (c3 ) = 0. Mais g 00 (x) = f 00 (x) − K
donc K = f 00 (c3 ).
(t − v)(t − u)
Ainsi, g(t) = 0 ⇐⇒ f (t) − f˜(t) = f 00 (c3 ) ce qui implique :
2
Zv
(v − t)(t − u) (v − u)3
|Eu,v (f )| 6 M2 dt = M2 .
u 2 12
Autrement dit, la méthode du trapèze apparaît comme une méthode du rectangle à qui on a ajouté un terme
b − a f (b) − f (a)
correctif × . Elle n’en reste pas moins moins intéressante que la méthode du point milieu,
n 2
puisque l’erreur de la méthode du trapèze composite vérifie :
Jean-Pierre Becirspahic
7.12 informatique commune
y = f (x)
u w v
Par construction f˜ = f lorsque f est une fonction polynomiale de degré inférieur ou égal à 2 ; en appliquant
la formule de quadrature aux fonctions polynomiales t 7→ 1, t 7→ t − u, t 7→ (t − u)(v − t) on obtient un système
linéaire dont la résolution nous fournit les valeurs des poids associés à cette méthode :
v − u = α0 + α1 + α2
α = α = 1 (v − u)
(v − u) 2 (v − u)
0 1
6
= α1 (v − u) + α2 ⇐⇒
2 2
4
α2 = (v − u)
(v − u)3
v −u 2
6
= α2
6 2
Théorème. — La méthode de Simpson est une méthode d’ordre 3, et si f est de classe C 4 , l’erreur de quadrature
vérifie :
(v − u)5
|Eu,v (f )| 6 M4
2880
où M4 est un majorant de |f (4) | sur l’intervalle [u, v].
Preuve. À première vue, puisque f est approché par un polynôme de Lagrange de degré 2 on devrait plutôt
s’attendre à ce que la méthode soit de degré 2. Néanmoins, il se trouve que la formule reste une égalité pour
tout polynôme de degré inférieur ou égal à 3. Pour le prouver, il suffit par linéarité de le vérifier pour un seul
polynôme de degré 3, par exemple f (x) = (x − u)3 :
Zv
1
Iu,v (f ) = (t − u)3 dt = (v − u)4
u 4
v − u 4(v − u) v−u 1 1 1
et I2u,v (f ) = f (u) + f (w) + f (v) = 0 + (v − u)4 + (v − u)4 = (v − u)4 .
6 6 6 12 6 4
Premiers algorithmes numériques 7.13
Tout comme pour la méthode du point milieu, l’astuce consiste ici à considérer non pas la fonction polynomiale
f˜ mais une fonction polynomiale p vérifiant : p(u) = f (u), p(v) = f (v), p(w) = f (w) et p0 (w) = f 0 (w). Puisque
nous venons de constater que la méthode est de degré 3 l’erreur de quadrature s’écrit encore :
Zv
Eu,v (f ) = (f (t) − p(t)) dt.
u
n−1 !
b−a X 1 4 uk + uk+1 1
f (uk ) + f + f (uk+1 ) .
n 6 6 2 6
k=0
(b − a)5
|En (f )| 6 M4
2280n4
Jean-Pierre Becirspahic
7.14 informatique commune
Pour p = 4 on obtient la méthode de Villarceau ; pour p = 6 la méthode de Hardy. Néanmoins, bien que
théoriquement plus efficaces que la méthode de Simpson, ces méthodes ne sont que peu ou pas utilisées car
plus le nombre de points d’interpolation augmente, plus les calculs à effectuer pour établir la formule sont
nombreux ; or qui dit calcul sur les nombres flottants dit erreurs d’approximations. Autrement dit, le gain
théorique que l’on pourrait espérer obtenir en appliquant ces méthodes se trouve contrebalancé par la succession
des erreurs d’approximation sur les nombres flottants. Ainsi, parmi toutes ces méthodes bases sur l’interpolation
de Lagrange la méthode de Simpson apparaît comme un bon compromis entre efficacité théorique et pratique.
>>> np.sqrt(np.pi)
1.7724538509055159
informatique commune
Chapitre 8
Résolution numérique des
équations
1. Introduction
1.1 Présentation du problème
Dans ce chapitre nous nous intéressons à l’approximation des zéros d’une fonction réelle d’une variable réelle.
Autrement dit, étant donné un intervalle I de R et une application f : I → R, on cherche à trouver au moins une
valeur approchée de c ∈ I (s’il en existe) tel que f (c) = 0.
Toutes les méthodes que nous allons présenter sont itératives et consistent donc en la construction d’une suite
(xn )n∈N telle que, on l’espère, lim xn = c. Nous verrons que la convergence de ces méthodes itératives dépend en
général du choix de la donnée initiale x0 . Ainsi, on ne sait le plus souvent qu’établir des résultats de convergence
locale, valables lorsque x0 appartient à un certain voisinage de c.
Remarque. Le cas des équations algébriques, c’est-à-dire de la forme p(x) = 0 où p est une fonction polynomiale,
est particulier : les polynômes possèdent un certain nombre de propriétés qui les distinguent des fonctions
quelconques. C’est la raison pour laquelle il existe des méthodes dédiées aux seuls polynômes. Ces méthodes ne
seront pas abordées dans ce chapitre.
Définition. — Soit (xn )n∈N une suite qui converge vers une limite c. On dit que la convergence de (xn )n∈N vers c est
d’ordre r > 1 lorsqu’il existe une suite (en )n∈N qui converge vers 0, majore l’erreur commise : ∀n ∈ N, |xn − c| 6 en et
vérifie :
e
lim n+1 = µ > 0.
enr
Une méthode itérative qui fournit en général des suites dont la convergence est d’ordre r sera elle-même dite
d’ordre r.
Remarque. Lorsque r = 1 on a nécessairement µ 6 1. En effet, si on avait µ > 1 la suite (en )n∈N serait strictement
croissante (à partir d’un certain rang) et ne pourrait converger vers 0.
Preuve. Dans le cas de la recherche d’une racine c dans l’intervalle [a, b] il a été établi au chapitre précédent
b−a b−a e 1
que |xn − c| 6 n . Posons en = n . Alors lim n+1 = .
2 2 en 2
http://info-llg.fr
8.2 informatique commune
Remarque. On peut observer que le cas µ = 1 ne permet pas de tirer de telles conclusions. Ces méthodes sont
dites sous-linéaires et sont en général très lentes.
Lorsque r = 2 l’approximation se traduit par δn+1 ≈ 2δn : à partir d’un certain rang le nombre de décimales
est doublé à chaque étape. De telles méthodes sont dites quadratiques ; leurs vitesses de convergence sont bien
supérieures à celles des méthodes linéaires.
Plus généralement, lorsque r > 1 le nombre de décimales exactes est à partir d’un certain rang multiplié par r à
chaque étape. On a donc tout à gagner à trouver des méthodes d’ordre le plus élevé possible.
c b x
a x
Figure 1 – Méthode de la fausse position. La recherche se poursuit dans l’intervalle [x, b].
L’idée est d’utiliser l’information fournie par les valeurs de la fonction f aux bornes de l’intervalle [a, b]
pour essayer d’approcher plus finement c que par le calcul du point milieu dans la méthode dichotomique.
Résolution numérique des équations 8.3
Concrètement, cela revient à calculer l’abscisse x de point d’intersection de la corde reliant les points de
coordonnées (a, f (a)) et (b, f (b)) avec l’axe des abscisses.
L’une des deux conditions f (a)f (x) 6 0 ou f (x)f (b) 6 0 est nécessairement vérifiée ; la recherche se poursuit
donc dans l’intervalle [a, x] ou [x, b] suivant les cas.
f (b) − f (a)
La corde en question a pour équation : y = (x − a) + f (a) donc l’abscisse x du point d’intersection avec
b−a
l’axe des abscisses vérifie :
f (b) − f (a) af (b) − bf (a)
(x − a) + f (a) = 0 ⇐⇒ x = .
b−a f (b) − f (a)
L’algorithme de recherche consiste donc à itérer deux suites (un )n∈N et (vn )n∈N définies par les valeurs initiales
u0 = a et v0 = b et la relation de récurrence :
(un , xn ) si f (un )f (xn ) 6 0
u f (vn ) − vn f (un )
avec xn = n
(un+1 , vn+1 ) =
(xn , vn ) sinon
f (vn ) − f (un )
Attention, la quantité vn − un (la largeur de l’intervalle de recherche) est une suite décroissante, mais contrai-
rement à la méthode dichotomique nous n’avons plus ici la garantie que cette suite tend vers 0. C’est le cas
en particulier lorsque la fonction est convexe ou concave. Dans ces cas de figure l’une des deux bornes reste
constante alors que l’autre converge de façon monotone vers c (illustration figure 2).
u0 = a u1 u2
c vn = b
Figure 2 – Cas d’une fonction convexe telle que f (a) < 0 < f (b) : la suite (un )n∈N est croissante et converge vers
c ; la suite (vn )n∈N est constante.
Il est donc impératif que pour le critère d’arrêt soit basé sur la valeur du résidu f (xn ) : puisque xn converge vers
un zéro c (voir le théorème ci-dessous) et que f est continue, on a lim f (xn ) = 0 et pour tout ε > 0 il existe un
rang n à partir duquel |f (xn )| 6 ε.
Théorème. — Soit f : [a, b] → R une fonction de classe C 1 vérifiant f (a)f (b) < 0 et strictement convexe (respective-
ment concave). Alors la suite (xn )n∈N construite par la méthode de la fausse position converge vers l’unique zéro c de
f . De plus, la convergence est linéaire (d’ordre 1).
Preuve. Supposons par exemple f strictement convexe et f (a) < 0 < f (b) (les autres cas se traitent de la même
façon), situation représentée figure 2.
Les fonctions convexes ont pour propriété d’avoir leurs cordes situées au dessus de leur graphe. Le point
d’intersection xn de la corde reliant les points d’abscisses un et vn est donc situé au dessus du point d’abscisse
xn de la courbe, ce qui montre que f (xn ) < 0. Ainsi, un+1 = xn et vn+1 = vn . La suite (vn )n∈N est constante égale à
b et la suite (un )n∈N croissante et majorée par b, donc convergente. Notons c sa limite.
xf (b) − bf (x) b−x
On a un+1 = g(un ) avec g : x 7→ = x − f (x) et par passage à la limite c = g(c), ce qui
f (b) − f (x) f (b) − f (x)
implique f (c) = 0. Nous avons bien prouvé la convergence de la méthode de la fausse position.
http://info-llg.fr
8.4 informatique commune
un+1 − c
Pour montrer que cette méthode est d’ordre 1 il faut encore prouver que lim = µ > 0. Pour ce faire on
un − c
un+1 − c g(un ) − g(c) u −c
observe que : = donc lim n+1 = g 0 (c).
un − c un − c un − c
f (b) − f (x) − (b − x)f 0 (x) f (b) − (b − c)f 0 (c)
On calcule g 0 (x) = f (b) 2
donc g 0 (c) = .
(f (b) − f (x)) f (b)
Les fonctions convexes ont pour propriété d’avoir leur graphe situé au dessus de leurs tangentes, donc en
particulier : f (b) > f (c)+(b−c)f 0 (c) = (b−c)f 0 (c), ce qui montre que g 0 (c) > 0. La convergence est bien linéaire.
Remarque. Lorsque f est de classe C 2 on aura le plus souvent f 00 (c) , 0, ce qui assure que f est strictement
convexe ou concave au voisinage de c. Dans ce cas, le théorème précédent assure que l’une des deux suites
(un )n∈N ou (vn )n∈N est stationnaire (c’est-à-dire constante à partir d’un certain rang) et que la convergence est
linéaire. Cette méthode ne présente donc par de gain notable comparativement à la méthode dichotomique.
y = ϕn (x)
c xn+1 xn
y = f (x)
f (xn )
Cette tangente a pour équation y = f (xn ) + f 0 (xn )(x − xn ) donc ϕn (xn+1 ) = 0 ⇐⇒ xn+1 = xn − .
f 0 (xn )
La méthode de Newton-Raphson consiste donc en l’itération d’une suite (xn )n∈N définie par la donnée d’une
valeur x0 et de la relation de récurrence :
f (xn )
xn+1 = xn − .
f 0 (xn )
Théorème. — Soit f une fonction de classe C 2 définie au voisinage d’un point c pour lequel f (c) = 0 et f 0 (c) , 0.
Alors il existe un voisinage V de c pour lequel, quel que soit x0 ∈ V la suite (xn )n∈N converge vers c. En outre, la
convergence est quadratique (c’est-à-dire d’ordre 2).
Preuve. Quitte à remplacer f par −f on suppose f 0 (c) > 0. La fonction f est donc strictement croissante au
voisinage de c, ce qui nous autorise à considérer un intervalle [a, b] sur lequel f 0 > 0 et pour lequel f (a) < 0 < f (b).
f (x)
Posons pour tout x ∈ [a, b] : ϕ(x) = x − 0 . La fonction ϕ est de classe C 1 sur [a, b] et f (x) = 0 ⇐⇒ ϕ(x) = x.
f (x)
Alors pour tout x ∈ [a, b],
f (x) f (x) − f (c) f (c) − f (x) − (c − x)f 0 (x)
ϕ(x) − c = x − c − = x − c − = .
f 0 (x) f 0 (x) f 0 (x)
Résolution numérique des équations 8.5
Théorème. — Soit f : [a, b] → R une fonction de classe C 1 , strictement croissante et convexe, telle que f (a) < 0 <
f (x )
f (b). Alors la suite définie par f (x0 ) > 0 et la relation xn+1 = xn − 0 n existe et converge quadratiquement vers
f (xn )
l’unique zéro c de f sur ]a, b[.
Preuve. Une fonction convexe a la propriété d’avoir son graphe situé au dessus de ses tangentes, en conséquence
de quoi, si c < xn < b alors c < xn+1 < xn . Ceci prouve que la suite (xn )n∈N est bien définie par le choix d’une
valeur x0 vérifiant c < x0 < b et que cette suite est décroissante et minorée par c. Elle possède donc une limite
qui est un point fixe de la fonction ϕ. Mais ϕ(x) = x ⇐⇒ f (x) = 0 donc (xn )n∈N converge vers c.
Enfin, avec les notations du théorème précédent, il existe un rang N à partir duquel xn ∈ V , ce qui assure la
convergence quadratique de la suite.
http://info-llg.fr
8.6 informatique commune
Pour fixer les idées, supposons qu’on travaille avec un ordinateur qui utilise une représentation décimale à trois
chiffres significatifs et que l’on souhaite calculer à l’aide de cette formule une valeur approchée de f 0 (7) avec
f : x 7→ x2 .
– Si on prend h = 0,1 on approche f 0 (7) = 14 par :
On aboutit à une situation paradoxale où c’est la valeur la plus grande de h qui donne la meilleure approximation
de la dérivée !
On comprend que pour choisir la valeur optimale de h il va falloir tenir compte de la représentation des
nombres en machine.
Nous savons que les nombres flottants sont représentés en machine en base 2 avec une mantisse de 52 bits.
Posons ε = 2−52 ; il s’agit de la précision relative de la machine (l’epsilon numérique).
L’erreur absolue sur l’évaluation d’une fonction f en un point x est de l’ordre de ε|f (x)| ; on peut donc estimer
l’erreur sur le numérateur par :
ε|f (x + h)| + ε|f (x)| ≈ 2ε|f (x)|.
f (x)
L’erreur absolue sur le quotient qui approche la dérivée peut donc être estimée par 2ε .
h
f (x + h) − f (x)
Il faut maintenant évaluer l’erreur que l’on commet en approchant f 0 (x) par le quotient . Pour
h
cela, on utilise la formule de Taylor :
h2 00
f (x + h) = f (x) + hf 0 (x) + f (x) + o(h2 )
2
f (x + h) − f (x) |h|
qui, lorsque f 00 (x) , 0, fournit l’équivalent : − f 0 (x) ∼ |f 00 (x)|.
h 2
0 f (x + h) − f (x)
L’erreur totale de l’approximation de f (x) par peut donc être évaluée par :
h
|f (x)| |h| 00
E(h) = 2ε + |f (x)|.
|h| 2
s
|f (x)|
Une étude de cette fonction sur [0, +∞[ montre que l’erreur est minimale pour h0 = 2 ε .
|f 00 (x)|
h 0 h0 +∞
0 −
E (h) 0 +
+∞ +∞
E(h)
E(h0 )
|f (x)| √
Dans des domaines dans lesquels le quotient reste borné on choisit en général h ≈ ε soit h = 10−8 .
|f 00 (x)|
Remarque. L’erreur mathématique peut être améliorée en observant que :
h2 00 h3 h2 00 h3
f (x + h) = f (x) + hf 0 (x) + f (x) + f (3) (x) + o(h2 ) et f (x − h) = f (x) − hf 0 (x) + f (x) − f (3) (x) + o(h2 )
2 6 2 6
Résolution numérique des équations 8.7
f (x + h) − f (x − h)
donc il est possible d’approcher f 0 (x) par et dans ce cas l’erreur commise vaut :
2h
f (x + h) − f (x − h) h2
− f 0 (x) ∼ |f (3) (x)|.
2h 3
Dans ce cas, l’erreur totale d’approximation vaut :
|f (x)| h2 (3)
E(h) = 2ε + |f (x)|.
|h| 3
s
3
3ε|f (x)|
et celle-ci est minimale pour h0 = .
|f (3) (x)|
f (x + h) − f (x − h) √
Lorsqu’on choisit d’approcher f 0 (x) par on choisit en général h ≈ 3 ε soit h = 10−5 .
2h
c
xn+1 xn xn−1
Théorème. — Soit f une fonction de classe C 2 définie au voisinage d’un point c pour lequel f (c) = 0 et f 0 (c) , 0.
2
Alors il existe un voisinage V de c pour lequel, quel que soit
√ (x0 , x1 ) ∈ V vérifiant x0 , x1 la suite (xn )n∈N converge
1+ 5
vers c. En outre, la convergence est d’ordre φ avec φ = ≈ 1,618.
2
Autrement dit, cette méthode est un peu moins bonne que la méthode de Newton-Raphson qui elle est
quadratique, mais reste bien meilleure que les méthodes linéaires évoquées au début de ce chapitre (en
particulier la méthode dichotomique).
http://info-llg.fr
8.8 informatique commune
Find a zero of the function func given a nearby starting point x0.
The Newton−Raphson method is used if the derivative fprime of func
is provided, otherwise the secant method is used.
Parameters
−−−−−−−−
func : function
The function whose zero is wanted.
x0 : float
An initial estimate of the zero that should be somewhere near the
actual zero.
fprime : function, optional
The derivative of the function when available and convenient. If it
is None (default), then the secant method is used.
tol : float, optional
The allowable error of the zero value.
maxiter : int, optional
Maximum number of iterations.
Returns
−−−−−
zero : float
Estimated location where function is zero.
On y apprend notamment que si la dérivée f 0 n’est pas fournie par l’utilisateur c’est la méthode de la sécante
qui est utilisée.
Pour finir, illustrons ce chapitre en considérant la fonction f : x 7→ 2x3 − 4x − 1, dont le graphe figure ci dessous.
8
6
4
2
0
2
4
6
8
10
2.0 1.5 1.0 0.5 0.0 0.5 1.0 1.5 2.0
On constate que cette fonction possède trois zéros dans l’intervalle [−2, 2] ; le script ci-dessous montre que
suivant la valeur de x0 choisie, la méthode de Newton-Raphson converge vers l’un de ces trois zéros.
Résolution numérique des équations 8.9
Si maintenant on omet de préciser la valeur de la dérivée, c’est la méthode de la sécante qui est appliquée, avec
pour conséquence une légère variation des résultats :
>>> newton(f, 0)
−0.25865202250415226
Dans tous les cas, le manque de fiabilité de la méthode impose une vérification des résultats a posteriori : pour
certaines valeurs de x0 il se peut que la méthode fournisse un résultat qui est éloigné d’un zéro de la fonction
(en particulier si on choisit pour critère d’arrêt une tolérance sur l’incrément |xn+1 − xn |). Il se peut aussi qu’il
n’y ait pas de convergence (ce qui explique la présente du paramètre maxiter) :
>>> def f(x): return x**3 − 2 * x + 2
>>> def df(x): return 3 * x**2 − 2
Dans l’exemple ci-dessus, la suite (xn )n∈N oscille indéfiniment entre les deux valeurs 0 et 1.
http://info-llg.fr
informatique commune
Chapitre 9
Résolution numérique des
équations différentielles
1. Méthodes d’Euler
1.1 Présentation du problème
Nous allons dans un premier temps nous intéresser aux équations différentielles que l’on peut mettre sous la
forme :
x0 = f (x, t)
où f est une fonction définie sur une partie U de R2 , à valeurs dans R.
Une solution de cette équation différentielle est une fonction x de classe C 1 définie sur un certain intervalle I de
R et à valeurs dans R vérifiant :
(i) ∀t ∈ I, (x(t), t) ∈ U ;
(ii) ∀t ∈ I, x0 (t) = f (x(t), t).
Nous allons adjoindre à cette équation différentielle une condition initiale sous la forme d’un couple (x0 , t0 ) ∈ U
et chercher à résoudre le problème de Cauchy suivant :
( 0
x = f (x, t)
x(t0 ) = x0
Sous certaines conditions sur f que nous ne détaillerons pas, ce problème admet une unique solution 1 , que
nous allons chercher à déterminer numériquement.
Exemple. Considérons le circuit électrique suivant :
R
e C s
i
ds ds
Il est régi par les relations s = e − Ri et i = C
qui conduisent à l’équation différentielle : RC + s = e.
dt dt
En posant τ = RC on est amené à résoudre le problème de Cauchy :
ds 1
dt = τ (e(t) − s)
q
s(0) = 0
C
où q0 désigne la charge initiale du condensateur.
Bien sur, il est possible de résoudre analytiquement cette équation lorsque la fonction d’entrée e(t) est connue.
Par exemple, dans le cas d’un signal d’entrée nul e(t) = 0 (décharge du condensateur) on obtient :
q
s(t) = 0 e−t/τ
C
Dans le cas d’un signal d’entrée sinusoïdal e(t) = Ecos(ωt) la résolution exacte reste possible mais plus délicate ;
néanmoins on peut obtenir :
q E E
s(t) = e−t/τ 0 −
+ cos(ωt) + ωτ sin(ωt)
C 1 + ω2 τ 2 1 + ω2 τ 2
Dans d’autres cas (par exemple la réponse du système à des impulsions périodiques) l’expression formelle qu’on
peut obtenir n’est plus forcément suffisante pour en permettre une analyse qualitative. On se tourne dans ce cas
vers les méthodes de résolution numérique.
1. Plus précisément, une unique solution maximale, c’est-à-dire ne pouvant être prolongée sur un intervalle plus grand.
http://info-llg.fr
9.2 informatique commune
• Champ de vecteurs
On appelle champ de vecteurs une application qui a tout point (x, t) de U associe un vecteur. Lorsque x(t) est
une solution de l’équation différentielle, le vecteur v~(t) = ~ı + x0 (t)~ = ~ı + f (x(t), t)~ est un vecteur tangent de cette
solution au point de paramètre t. Ceci permet d’associer naturellement à l’équation différentielle x0 = f (x, t) le
champ de vecteurs défini sur U par : v~(x, t) = ~ı + f (x, t)~.
On représente en général un champ de vecteurs en des points régulièrement espacés de U et en normalisant
les vecteurs. La fonction quiver du module matplotlib.pyplot permet de tracer un champ de vecteurs.
Utilisons-la pour obtenir celui associé à l’équation différentielle s0 = −s/τ (décharge d’un condensateur). La
courbe rouge est la solution déterminée analytiquement.
1.0
0.5
0.0
0.5
1.0
0.000 0.002 0.004 0.006 0.008 0.010
La méthode d’Euler explicite consiste à approcher cette intégrale par la méthode du rectangle gauche, autrement
Z tk+1
dit à approcher f (x(t), t) dt par (tk+1 − tk )f (x(tk ), tk ).
tk
En posant hk = tk+1 − tk , ceci conduit à définir une suite de valeurs x0 , x1 , . . . , xn à partir de la condition initiale
x0 et de la relation de récurrence :
On observera qu’en général, seul le premier point x0 de cette méthode est une valeur exacte ; les autres points
sont calculés à partir de l’approximation précédente, ce qui peut conduire la valeur calculée xk à s’écarter
de plus en plus de la valeur exacte x(tk ). Ce phénomène peut être particulièrement marqué pour la méthode
d’Euler ; on observera ce phénomène figure 2 avec la résolution du problème de Cauchy x0 = x, x(0) = 1 (dont
la solution exacte est x(t) = et ).
On se convaincra aisément que lorsque la subdivision s’affine la précision en général s’améliore (sans pour
autant faire disparaître ce phénomène de divergence). D’autres méthodes décrites plus loin permettent de
Résolution numérique des équations différentielles 9.3
8 8
solution exacte solution exacte
7 solution approchée 7 solution approchée
6 6
5 5
4 4
3 3
2 2
1 1
0.0 0.5 1.0 1.5 2.0 0.0 0.5 1.0 1.5 2.0
1.0 1.0
solution exacte solution approchée
0.8 0.8
0.6 0.6
0.4 0.4
0.2 0.2
0.0 0.0
0.2 0.2
0.000 0.002 0.004 0.006 0.008 0.010 0.000 0.002 0.004 0.006 0.008 0.010
Figure 3 – Réponse d’un circuit RC à une sollicitation sinusoïdale par la méthode d’Euler avec n = 200.
Remarque. Les exemples présentés utilisent tous une subdivision de pas régulier ; cependant de meilleurs
résultats sont obtenus en adaptant le pas à la fonction f : si f (xk , tk ) est faible alors x varie peu et on peut utiliser
un pas plus grand. On contraire, on réduira le pas lorsque la valeur de f (xk , tk ) augmente. Dans ce cas, on parle
de méthode à pas adaptatif, mais nous n’irons pas plus loin sur ce sujet.
http://info-llg.fr
9.4 informatique commune
trouvera figure 4 une comparaison des deux méthodes d’Euler qui met en évidence l’instabilité de la méthode
explicite sur le long terme. La solution de l’équation x0 = t sin(x) avec la condition initiale x(0) = 1 est une
fonction croissante qui tend vers π lorsque t tend vers +∞.
5
euler explicite
euler implicite
4
0
0 5 10 15 20
Sur un intervalle de temps par trop important les deux méthodes donnent des résultats comparables. Cependant,
à partir d’un certain rang l’instabilité sur le long terme de la méthode explicite se révèle. En effet, la relation
d’Euler explicite s’écrit :
xk+1 = xk + htk sin(xk )
où h (fixé) est le pas de la subdivision choisi.
Puisque xk est voisin de π le terme sin(xk ) est proche de 0 mais non nul. Dans le même temps, le terme tk ne
cesse de croître et prend progressivement le pas sur le terme sin(xk ). Dès lors, le comportement de la suite x
devient chaotique : lorsque la valeur calculée xk s’écarte un peu trop de π, le comportement de la suite devient
imprévisible.
En revanche, l’expérience montre que même sur un grand intervalle de temps la solution fournie par la méthode
d’Euler implicite continue de fournir un résultat cohérent.
Par exemple, la méthode d’Euler explicite correspond au schéma φ(x, t, h) = f (x, t).
Autrement dit, φ(x, t, h) correspond à la valeur que l’on choisit pour approcher la dérivée entre t et t + h :
xk+1
xk pente = φ(xk , tk , hk )
tk tk + hk
Résolution numérique des équations différentielles 9.5
Remarque. Un schéma numérique implicite à un pas prend la forme xk+1 = xk + hk φ(xk , xk+1 , tk , hk ) ; c’est le cas
par exemple de la méthode d’Euler implicite.
Un schéma numérique explicite à p pas prend la forme xk+1 = xk + hk φ(xk , xk−1 , . . . , xk−p+1 , tk , hk ). Le calcul de xk
n’est possible qu’à partir de l’indice p et la méthode doit être complétée par un calcul initial des p premières
valeurs, par exemple par une méthode à un pas.
Aucun de ces deux schémas ne sera abordé dans la suite de ce cours.
Définition. — On appelle erreur de consistance la suite (ek )16k6n définie par ek+1 = x(tk+1 )−x(tk )−hk φ(x(tk ), tk , hk ),
où x désigne la solution exacte du problème de Cauchy.
x3
x2
x1
e2
e1
e3
t0 t1 t2 t3
Attention. La consistance d’une méthode est une condition nécessaire mais pas suffisante pour que la solution
approchée converge uniformément vers la solution exacte sur le segment [t0 , t0 + T].
Considérons l’illustration de la figure 5. À la date t1 , e1 représente effectivement l’écart entre la solution exacte
et la solution approchée :
x(t1 ) = x1 + e1
Mais dès la date t2 , à l’erreur de consistance s’ajoute le fait que le calcul de la pente n’a pas été fait pour x = x(t1 )
mais pour x = x1 . Ainsi, on a :
(
x2 = x1 + h1 φ(x1 , t1 , h1 )
donc x(t2 ) = x2 + (e1 + e2 ) + h1 φ(x(t1 ), t1 , h1 ) − φ(x1 , t1 , h1 )
x(t2 ) = x(t1 ) + h1 φ(x(t1 ), t1 , h1 ) + e2
De même, on obtient :
x(t3 ) = x3 + (e1 + e2 + e3 ) + h1 φ(x(t1 ), t1 , h1 ) − φ(x1 , t1 , h1 ) + h2 φ(x(t2 ), t2 , h2 ) − φ(x2 , t2 , h2 )
| {z } | {z }
erreurs de consistance erreurs dans l’évaluation de φ
L’évaluation de l’erreur totale commise sur l’intervalle [t0 , t0 + T] est un problème très délicat que nous
n’aborderons pas ; nous nous contenterons du résultat suivant :
Théorème. — Lorsque la fonction f est de classe C 1 la méthode d’Euler est une méthode consistante d’ordre 1.
Ainsi, x(tk+1 ) − x(tk ) − hk f (x(tk ), tk ) = O(h2k ), soit ek+1 = O(h2k ). La méthode d’Euler est bien d’ordre 1.
http://info-llg.fr
9.6 informatique commune
Pour montrer que la méthode d’Euler est consistante, reprenons ces calculs mais en appliquant cette fois
l’égalité de Taylor-Lagrange : puisque x est de classe C 2 il existe τk ∈ ]tk , tk+1 [ tel que :
h2k 00 h2
x(tk+1 ) = x(tk ) + hk x0 (tk ) + x (τk ) = x(tk ) + hk f (x(tk ), tk ) + k x00 (τk )
2 2
h2k 00
Autrement dit, |ek+1 | = |x (τk )|. Puisque x est de classe C 2 sur le segment [t0 , t0 + T] sa dérivée seconde est
2
M
majorée en valeur absolue par une constante M2 et dans ce cas, |ek+1 | 6 2 h2k .
2
T M T2
Dans le cas d’une subdivision de pas régulier nous avons hk = et |ek+1 | 6 2 2 . Ainsi,
n 2n
n n n
X X M2 T X
ek 6 |ek | 6 et lim ek = 0.
2n n→+∞
k=1 k=1 k=1
Mn+1
Mn
tn tn + hn /2 tn+1
Figure 6 – En général, la pente de la tangente au point milieu est une bonne approximation de la pente de la
corde.
D’où l’idée de remplacer la relation d’Euler xn+1 = xn + hn f (xn , tn ) par : xn+1 = xn + hn f (x(tn + hn /2), tn + hn /2).
Cependant, comme on ne connait pas x(tn + hn /2), on en cherche une approximation notée xn+1/2 ; le schéma
h
d’Euler nous suggère de prendre xn+1/2 = xn + n f (xn , tn ).
2
On aboutit donc au schéma numérique suivant :
hn
xn+1/2 = xn + f (xn , tn ) puis xn+1 = xn + hn f (xn+1/2 , tn + hn /2)
2
h h
qui correspond au schéma φ(x, t, h) = f x + f (x, t), t + .
2 2
Nous admettrons que sous des hypothèses suffisantes la méthode du point milieu est une méthode consistante
d’ordre 2.
On comparera figure 7 les méthodes d’Euler et du point milieu pour résoudre le problème de Cauchy x0 = x,
x(0) = 1 sur l’intervalle [0, 2] avec n = 4.
Résolution numérique des équations différentielles 9.7
8
solution exacte
7 méthode d'Euler
méthode du point milieu
6
1
0.0 0.5 1.0 1.5 2.0
où les coefficients ci , aij et bi sont des constantes qui définissent précisément le schéma. On supposera toujours
i−1
X s
X
dans la suite que c1 = 0, ci = aij et bi = 1.
j=1 i=1
Par exemple, la méthode de du point milieu est une méthode de type RK2 puisqu’elle se définit par les relations :
k1 = f (x, t)
k2 = f (x + hk1 /2, t + h/2)
φ(x, t, h) = k2
On a ici c2 = 1/2, a21 = 1/2, b1 = 0, b2 = 1.
On représente usuellement une méthode RKs à l’aide d’un tableau de Butcher :
c1
c2 a21
c3 a31 a32
.. .. .. ..
. . . .
cs as1 as2 ··· as,s−1
b1 b2 ··· bs−1 bs
http://info-llg.fr
9.8 informatique commune
Ainsi, les méthodes d’Euler et du point milieu sont des méthodes de types respectifs RK1 et RK2 définies par
les tableaux :
0
0 1/2 1/2
1 et 0 1
Sous de bonnes hypothèses et en choisissant de manière adéquate les coefficients de ces tableaux, une méthode
de Runge-Kutta explicite à s étages définit un schéma d’ordre s.
Il existe de nombreuses méthode de ce type, les plus fréquentes sont les suivantes :
0
0 1/2 1/2
1 0 1
0
1/2 1/2
0 1/2 0 1/2
1 1 1 0 0 1
1/2 1/2 1/6 2/6 2/6 1/6
k1 = f (x, t)
k2 = f (x + hk1 , t + h)
φ(x, t) = k1 /2 + k2 /2
1
soit φ(x, t) = f (x, t) + f (x + hf (x, t), t + h) . Cette méthode fait intervenir la moyenne des dérivées en t et en
2
t + h ; lorsque les solutions sont convexes, la première constitue un minorant de la dérivée entre t et t + h, la
seconde un majorant. La moyenne des deux est donc encadrée par ces deux bornes.
Exercice. Rédiger les formules correspondant à la méthode RK4 .
La méthode RK4 est parmi les méthodes explicites la plus couramment utilisée. Elle fournit en général
d’excellents résultats.
Ces p fonctions numériques constituent les p composantes de la fonction vectorielle X à valeurs dans Rp et
définie par : X = (x1 , x2 , . . . , xp ).
Considérons maintenant la fonction vectorielle F définie sur Rp × R et à valeurs dans Rp dont les fonctions
f1 , . . . , fp sont les composantes :
F(x1 , x2 , . . . , xp , t) = f1 (x1 , x2 , . . . , xp , t), f2 (x1 , x2 , . . . , xp , t), . . . , fp (x1 , x2 , . . . , xp , t) .
Résolution numérique des équations différentielles 9.9
X0 = F(X, t)
et dès lors on peut lui appliquer les méthodes de résolution numériques qui ont été présentées dans la section
précédente. Autrement dit, plutôt que de voir dans le problème à résoudre un système d’équations différentielles
couplées, il est plus fécond de n’y voir qu’une seule équation qui concerne une fonction à valeurs vectorielles.
Exemple. Considérons le circuit électrique suivant :
R L
e C s
i
di ds
Il est régi par les relations s = e − Ri − L et i = C qui conduisent au système différentiel :
dt dt
di 1
dt = L (−Ri − s + e)
ds i
=
dt C
Bien entendu, la résolution exacte est possible en combinant ces deux équations pour obtenir une équation
du second ordre vérifiée par la fonction s, mais la démarche numérique consiste à considérer plutôt le vecteur
1 i
X = (i, s) solution de l’équation différentielle X0 = F(X, t) avec F (i, s), t = −Ri − s + e(t) , .
L C
Dans le cas vectoriel, la méthode d’Euler consiste à discrétiser l’intervalle de temps [t0 , t0 + T] et à calculer la
suite de vecteurs (Xk )16k6n à l’aide des formules :
4
tension aux bornes du condensateur
3
4
0.000 0.005 0.010 0.015 0.020
Bien entendu, toutes les méthodes décrites pour les équations différentielles scalaires s’appliquent encore aux
équations différentielles vectorielles.
http://info-llg.fr
9.10 informatique commune
Dès lors, on peut appliquer une quelconque des méthodes de résolutions numériques que nous avons étudiées.
Par exemple, la méthode d’Euler consiste à discrétiser l’intervalle de temps [t0 , t0 + T] et à calculer la suite de
valeurs (θk , θ̇k )16k6n à partir des valeurs initiales (θ0 , θ̇0 ) et des relations :
θk+1 = θk + hk θ̇k
avec hk = tk+1 − tk .
θ̇k+1 = θ̇k − ω2 hk sin(θk )
résolution numérique
1.0 approximation des petites oscillations
0.5
0.0
0.5
0 5 10 15 20 25 30
Figure 10 – La résolution numérique montre que l’approximation dite « des petites oscillations » sous-estime la
valeur de la période.
Cette fonction est dédiée à la résolution des systèmes numériques, mais s’applique aussi aux équations scalaires
(qui somme toute ne sont que des systèmes différentiels d’ordre 1) et aux équations différentielles d’ordre
supérieur, à charge pour l’utilisateur de convertir cette dernière sous la forme d’un système différentiel, comme
nous venons de l’expliquer.
Examinons une version simplifiée de l’aide en ligne :
odeint(func, y0, t)
Integrate a system of ordinary differential equations.
dy/dt = func(y,t0)
Parameters
−−−−−−−−
func : callable (y, t0)
Computes the derivative of y at t0.
y0 : array
Initial condition on y (can be a vector).
t : array
A sequence of time points for which to solve for y. The initial
value point should be the first element of this sequence.
Returns
−−−−−
y : array, shape ( len (t), len (y0))
Array containing the value of y for each desired time in t,
with the initial value y0 in the first row.
On notera que le premier paramètre de odeint est une fonction chargée d’exprimer la dérivée première de y
en fonction de y et de t : c’est la fonction f qui représente l’équation différentielle à résoudre y 0 = f (y, t). Le
second paramètre est la condition initiale y0 (qui suivant les cas sera un scalaire ou un vecteur) et le troisième
un tableau contenant les valeurs discrètes choisies dans l’intervalle [t0 , t0 + T] et dont le premier terme est t0 .
La valeur retournée par la fonction odeint est un tableau de même longueur que t et qui contient les valeurs
calculées par la méthode de résolution numérique. Attention, chaque case de ce tableau y a même dimension
que y0 (c’est un scalaire lorsqu’on résout une équation différentielle scalaire, c’est un vecteur lorsqu’on résout
un système différentiel).
plot (t, s)
http://info-llg.fr
9.12 informatique commune
On notera l’usage qui est fait du slicing pour extraire de x les valeurs de s. En effet, le tableau x contient n + 1
vecteurs de la forme [ik , sk ], un pour chacune des valeurs tk comprises dans le tableau t :
h i
x = [i0 , s0 ], [i1 , s1 ], . . . , [ik , sk ], . . . , [in , sn ]
h i
Le slicing x[: , 1] extrait de ce tableau le tableau x[k, 1] 0 6 k 6 n autrement dit :
x[: , 1] = [s0 , s1 , . . . , sk , . . . , sn ]
Pour obtenir le tracé de l’évolution de l’intensité i il suffirait d’écrire de même : plot(t, x[:, 0]).
Remarque. Sachant que les valeurs de θ sont rangées dans le tableau x[:, 0] et celles de θ̇ dans x[:, 1] il
suffit pour obtenir le diagramme des phases (c’est-à-dire la courbe paramétrée par θ(t) en abscisse et θ̇(t) en
ordonnée) d’écrire :
plot(x[:, 0], x[:, 1])
0.6
0.4
0.2
0.0
0.2
0.4
0.6
0.8
0.8 0.6 0.4 0.2 0.0 0.2 0.4 0.6 0.8
ce qui nous permettra de visualiser aisément les functions de ce module : elles seront préfixées par .np.
La matrice que nous venons de créer est une matrice à coefficients entiers car tous ses éléments sont des
entiers ; si on change l’un des coefficients, le nouveau coefficient sera au préalable converti en entier, ce qui peut
provoquer une confusion :
>>> a[0, 0] = 1.25
>>> a
array([[ 1, 6, 2, 0],
[ 2, −3, 5, −1],
[ 0, 6, 1, −1]])
Si la liste des éléments est hétérogène, certains seront automatiquement convertis. Par exemple, si la liste des
coefficients contient des éléments de type float et de type int, ces derniers seront convertis en flottants :
>>> a = np.array([[3., 6, 2, 0], [2, −3, 5, −1], [0, 6, 1, −1]])
>>> a
array([[ 3., 6., 2., 0.],
[ 2., −3., 5., −1.],
[ 0., 6., 1., −1.]])
Aussi, pour éviter toute ambiguïté il est préférable de préciser lors de la création le type des éléments souhaités
avec le paramètre optionnel dtype (pour data type) :
>>> b = np.array([1, 7, −1, 0, −2], dtype=float)
>>> b
array([ 1., 7., −1., 0., −2.])
1. On peut tout au plus les redimensionner, comme cela sera expliqué plus loin.
http://info-llg.fr
10.2 informatique commune
plus un certain nombre de types dont nous n’auront que peu ou pas d’usage : entiers signés sur 8-16-32-64 bits,
entiers non signés sur 8-16-32-64 bits, flottants sur 8-16-32-64 bits, . . .
Remarque. Attention, en réalité le data type int utilisé par numpy ne correspond pas au type int de Python ; il
s’agit du type int64 des entiers signés sur 64 bits (codés par complémentation à deux). Autrement dit, les entiers
numpy sont restreints à l’intervalle ~−263 , 263 − 1.
Dorénavant, et sauf mention explicite du contraire, nous supposerons les tableaux numpy remplis à l’aide du
type float (correspondant sur les machines actuelles au data type float64 des nombres flottants représentés sur
64 bits).
1.2 Coupes
On accède à un élément d’un tableau numpy exactement de la même façon que pour une liste : à partir de son
indice.
>>> b[0]
1.0
Pour les tableaux multidimensionnels, outre la syntaxe usuelle a[i][j] il est aussi possible d’utiliser la syntaxe
a[i, j] :
>>> a[0, 0]
3.0
Le slicing suit la même syntaxe que pour les listes Python. On retiendra surtout la syntaxe pour obtenir une
vue d’une colonne ou d’une ligne d’une matrice :
>>> a[2] # 3e ligne de la matrice a
array([ 0., 6., 1., −1.])
Attention cependant, à la différence des listes Python, une coupe d’un tableau numpy ne crée pas un nouveau
tableau (la terminologie officielle parle dans ce cas de « vue » plutôt que de coupe). Pour copier un tableau
numpy il est donc indispensable d’utiliser la méthode copy.
>>> a1 = a[2] # a1 est une vue de la 3e ligne de a
>>> a[2, 0] = 7
>>> a2
array([ 0., 6., 1., −1.]) # la modification de a ne se répercute pas sur a2
Résolution numérique d’un système linéaire 10.3
Cette différence avec les listes Python peut s’avérer problématique lorsqu’il s’agit d’effectuer des opération
élémentaires sur les lignes ou les colonnes d’une matrice. En particulier, la syntaxe a[i], a[j] = a[j], a[i]
n’échange pas les lignes (i + 1) et (j + 1).
>>> a
array([[ 2., −3., 5., −1.],
[ 2., −3., 5., −1.],
[ 7., 6., 1., −1.]])
Il est donc sage lorsqu’on manipule des tableaux numpy de s’interdire l’affectation simultanée et lui préférer
une syntaxe telle que :
b = a[0].copy()
a[0] = a[1]
a[1] = b
voire, pour éviter tout coût spatial, à permuter terme par terme chaque élément des lignes à échanger :
for j in range(a.shape[1]):
a[0, j], a[1, j] = a[1, j], a[0, j]
« fancy indexing »
On peut néanmoins accéder au contenu d’un tableau numpy tant en lecture qu’en écriture en utilisant le fancy
indexing. Ici les positions dans un tableau ne procèdent plus nécessairement par des coupes mais peuvent être
données dans un ordre quelconque : si a est un tableau et l une liste d’indices, alors a[l] renvoie le tableau
formé des éléments a[i] où i est dans l. Il est donc possible d’échanger les lignes i et j du tableau a en écrivant :
>>> a = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]], dtype=float)
>>> a.shape
(3, 4)
Modifier cet attribut permet de redimensionner le tableau, à condition bien sur que le nombre total d’éléments
du tableau reste inchangé. Les 12 éléments qui composent le tableau a peuvent donc être ré-ordonnés pour
former un tableau de taille 2 × 6, voire un vecteur de taille 12 :
>>> a.shape = 2, 6
>>> a
array([[ 1., 2., 3., 4., 5., 6.],
[ 7., 8., 9., 10., 11., 12.]])
>>> a.shape = 12
>>> a
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.])
C’est l’occasion d’observer que numpy différencie les tableaux uni-dimensionnels (les vecteurs) des tableaux
bi-dimensionnels, même lorsque ces derniers ne comportent qu’une seule ligne ou une seule colonne :
http://info-llg.fr
10.4 informatique commune
>>> a.shape = 1, 3
>>> a # a est une matrice 1 x 3
array([[ 1., 2., 3.]])
>>> a.shape = 3, 1
>>> a # a est une matrice 3 x 1
array([[ 1.],
[ 2.],
[ 3.]])
On notera que redimensionner une matrice est une opération de coût constant. L’explication est simple : quelle
que soit la forme de la matrice, ses coefficients sont stockés en mémoire dans des cases contiguës et le format de
la matrice dans un emplacement spécifique. Par exemple, le script suivant :
>>> a = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], dtype=float)
>>> a.shape = 3, 4
>>> a
array([[ 1., 2., 3., 4.],
[ 5., 6., 7., 8.],
[ 9., 10., 11., 12.]])
crée un espace mémoire de 12 cases des 64 bits chacune ainsi qu’un espace mémoire supplémentaire contenant
différentes informations, en particulier que a est un tableau bi-dimensionnel de 3 lignes et de 4 colonnes.
Connaître la case dans laquelle se trouve l’élément de rang (i, j) n’est dès lors que le résultat d’un simple calcul
arithmétique : pour une matrice de n lignes et p colonnes il s’agit de la case de rang i × p + j, et modifier les
dimensions d’un tableau consiste tout simplement à changer cette formule, pas les emplacements en mémoire
des éléments.
Remarque. Il existe aussi une méthode reshape qui crée une nouvelle matrice (les éléments sont donc copiés)
en la redimensionnant si nécessaire (cette méthode a donc un coût proportionnel au nombre d’éléments de la
matrice).
La fonction diag appliquée à une liste renvoie la matrice diagonale formée à partir des coefficients de cette
liste :
2. Notez que les parenthèses pour enclore ce tuple sont indispensables si le tableau est multi-dimensionnel.
Résolution numérique d’un système linéaire 10.5
On prendra bien garde au fait que chacune de ces trois opérations élémentaires modifie physiquement la
matrice a.
2. une deuxième étape dite de remontée qui consiste à transformer la matrice inversible A0 en une matrice
A00 diagonale tout en effectuant les mêmes opérations sur b ;
x = ⇐⇒ x =
3. une troisième et dernière étape de résolution du système linéaire, devenu trivial car diagonal.
http://info-llg.fr
10.6 informatique commune
• Le choix du pivot
Mathématiquement, tous les pivots non nuls se valent. Il n’en est pas de même du point de vue numérique :
diviser par un pivot dont la valeur absolue est trop faible par rapport aux autres coefficients du système conduit
à des erreurs d’arrondi importantes. Nous allons illustrer ce phénomène à l’aide d’un exemple numérique.
Exemple. Supposons que les calculs sont effectués en virgule flottante dans le système décimal avec une
mantisse de quatre chiffres et considérons le système :
En effet, 1,764 · 103 × 5,914 · 101 ≈ 1,043 · 105 et −6,130 · 100 − 1,043 · 105 ≈ −1,043 · 105
1,764 · 103 × 5,917 · 101 ≈ 1,044 · 105 et 4,678 · 101 − 1,044 · 105 ≈ −1,044 · 105
L’étape de remontée qui sera expliquée plus loin consiste à effectuer l’opération élémentaire L1 ← L1 − µL2 pour
obtenir un système diagonal, avec
5,914 · 101
µ= ≈ −5,670 · 10−4
−1,043 · 105
et conduit à résoudre le système diagonal :
car 5,670 · 10−4 × 1,044 · 105 ≈ 5,919 · 101 et 5,917 · 101 − 5,919 · 101 ≈ −2,000 · 10−2 .
Ainsi, la solution numérique obtenue est : x ≈ −6,667 et y ≈ 1,001. Même si l’erreur faite sur y est très faible (de
l’ordre de 0, 1%), sa propagation a un effet désastreux sur x (une erreur de l’ordre de 167%), à cause d’un pivot
très faible.
4. L’inversibilité de la matrice A nous assure de son existence (voir votre cours de mathématiques).
Résolution numérique d’un système linéaire 10.7
Reprenons ces calculs, mais en choisissant cette fois 5,291 pour pivot, ce qui conduit à débuter par la permutation
L1 ↔ L2 des deux lignes du système :
5,291 · 100 −6,130 · 100 x 4,678 · 101
! ! !
=
3,000 · 10−3 5,914 · 101 y 5,917 · 101
On effectue cette fois l’opération L2 ← L2 − λL1 avec
0,003
λ= ≈ 5,670 · 10−4
5,291
ce qui conduit à résoudre le système triangulaire :
5,291 · 100 −6,130 · 100 x 4,678 · 101
! ! !
=
0 5,914 · 101 y 5,914 · 101
−6,130 · 100
et enfin, l’opération L1 ← L1 − µL2 avec µ = ≈ −1,037 · 10−1 conduit au système diagonal :
5,914 · 101
5,291 · 100 5,291 · 101
! ! !
0 x
=
0 5,914 · 101 y 5,914 · 101
qui fournit finalement les valeurs x ≈ 1,000 · 101 et y ≈ 1,000 · 100 qui coïncident avec les résultats théoriques.
Pivot partiel
Ces considérations nous amènent à choisir pour pivot le plus grand en module des coefficients ajj , aj+1,j , . . . , anj ;
c’est le choix du pivot partiel de Gauss.
Remarque. La méthode du pivot total consiste à cherche le plus grand en module des coefficients du bloc
rectangulaire
ajj · · · ajn
. ..
.
. .
anj · · · ann
Cependant, cela induit une difficulté algorithmique supplémentaire car pour amener ce pivot à l’emplacement
(j, j) il est souvent nécessaire d’effectuer une permutation entre deux colonnes qui modifie l’ordre des inconnues
pour le système linéaire à résoudre. Il faut donc garder trace de ces multiples permutations afin de renvoyer
celles-ci dans le bon ordre à la fin du calcul.
En pratique, on constate que le gain de stabilité apporté par la recherche du pivot total n’est en général pas
significatif, alors qu’il alourdit la programmation. C’est donc la recherche du pivot partiel qui est le plus
couramment utilisé, et c’est celui que nous allons implémenter.
http://info-llg.fr
10.8 informatique commune
et en utilisant ajj comme pivot pour éliminer les coefficients a1j , . . . , aj−1,j .
a) Rédiger une fonction determinant(A) qui calcule le déterminant de la matrice A. On travaillera sur une
copie de A pour ne pas modifier la matrice initiale.
Calcul de l’inverse
La méthode du pivot conduit à passer de la matrice A à la matrice In par une succession d’opérations élémen-
taires sur les lignes. En appliquant la même succession d’opérations sur la matrice In on obtient la matrice
A−1 .
b) Rédiger une fonction inverse(A) qui calcule l’inverse de la matrice A par la méthode du pivot. On
travaillera sur une copie de A pour le pas modifier cette dernière.
Résolution numérique d’un système linéaire 10.9
2 4 −4 1 x1 0
3 6 1 −2 x2 −7
=
−1 1 2 3 x3 4
1 1 −4 1 x4 2
>>> gauss(A, b)
array([ 1., −1., −0., 2.])
>>> la.solve(A, b)
array([ 1., −1., −0., 2.])
Le module numpy.linalg contient en outre une fonction det pour calculer le déterminant ainsi qu’une fonction
inv pour calculer l’inverse :
>>> determinant(A)
−28.000000000000014
>>> la.det(A)
−28.000000000000021
>>> inverse(A)
array([[−3.5 , 1.5 , 0.75 , 4.25 ],
[ 1.5 , −0.5 , −0.25 , −1.75 ],
[−0.78571429, 0.35714286, 0.25 , 0.75 ],
[−1.14285714, 0.42857143, 0.5 , 1.5 ]])
>>> la.inv(A)
array([[−3.5 , 1.5 , 0.75 , 4.25 ],
[ 1.5 , −0.5 , −0.25 , −1.75 ],
[−0.78571429, 0.35714286, 0.25 , 0.75 ],
[−1.14285714, 0.42857143, 0.5 , 1.5 ]])
http://info-llg.fr
10.10 informatique commune
def determinant(A):
U = A.copy()
d = 1
for j in range(U.shape[1] − 1):
# recherche du pivot
p = j
for i in range(j+1, U.shape[0]):
if U[i, j] > U[p, j]:
p = i
# permutation des deux lignes
if p != j:
d *= −1
U[[p, j]] = U[[j, p]]
# élimination
for i in range(j+1, U.shape[0]):
U[i] = U[i] − (U[i, j] / U[j, j]) * U[j]
# produit des éléments diagonaux
for k in range(U.shape[0]):
d *= U[k, k]
return d
def inverse(A):
return gauss(A, np.identity(A.shape[0]))
informatique commune
Chapitre 11
Manipulation de fichiers
Les données que l’on peut souhaiter manipuler (textes, images, . . .) sont souvent fournies sous forme de fichiers ;
l’objectif de ce chapitre est d’apprendre à effectuer un certain nombre de tâches les concernant, en particulier :
– ouvrir un fichier ;
– lire le contenu d’un fichier ;
– modifier le contenu d’un fichier ;
– fermer un fichier.
Dans l’exemple ci-dessus, le répertoire courant est celui de l’utilisateur Bob 1 (dans cet exemple le chemin est
indiqué suivant les conventions unix).
Nous avons ici utilisé la description relative du répertoire à partir du répertoire courant (représenté, rappelons-
le, par un point), mais nous aurions pu évidemment utiliser sa description absolue pour obtenir le même
résultat :
1. voir le chapitre 1 de ce cours.
Jean-Pierre Becirspahic
11.2 informatique commune
>>> os.listdir('/home/bob/travail')
['doc1']
Il existe dans le module os bien d’autres fonctions permettant d’interagir avec le système d’exploitation mais
dont nous n’aurons pas l’usage par la suite.
La première chose à faire est d’ouvrir ce fichier, à l’aide de la commande open. Cette fonction prend deux
arguments : le premier est une chaîne de caractères décrivant (sous forme relative ou absolue) le chemin menant
au fichier à ouvrir, le second indiquant le mode d’ouverture : 'r' (comme read) pour lire le contenu du fichier,
'w' (comme write) pour écrire dans ce fichier, 'a' (comme append) pour ajouter du texte à la suite de ce fichier,
etc.
Pour l’instant, nous voulons seulement lire son contenu, donc nous écrivons :
>>> comptine = open('exemple.txt', 'r')
Nous venons de créer une objet nommé comptine faisant référence au fichier exemple.txt :
>>> comptine
<_io.TextIOWrapper name='exemple.txt' mode='r' encoding='UTF−8'>
Nous allons désormais faire agir des méthodes sur cet objet, qui se répercuteront sur le fichier lié.
Rappelons que dans une chaîne de caractères, le passage à la ligne est représenté par le caractère spécial \n.
Cette méthode est la plus simple, mais n’est évidemment adaptée que si le fichier ne contient pas un texte trop
long. Si nécessaire, on peut préciser en argument de la méthode read le nombre de caractères à lire. Voici par
exemple une façon de procéder pour lire les caractères par groupe de 10 :
>>> comptine = open('exemple.tex', 'r')
>>> lst = []
>>> while True:
... txt = comptine.read(10)
... if len(txt) == 0:
... break
... lst.append(txt)
>>> lst
['Am, stram,', ' gram,\nPic', ' et pic et', ' colégram,', '\nBour et b',
'our et rat', 'atam,\nAm, ', 'stram, gra', 'm.']
Manipulation de fichiers 11.3
Mieux encore, un parcours par énumération est possible, l’énumération du fichier se faisant ligne par ligne :
>>> n = 0
>>> for l in comptine:
... n += 1
... print('{} :'.format(n), l, end='')
1 : Am, stram, gram
2 : Pic et pic et colégram,
3 : Bour et bour et ratatam,
4 : Am, stram, gram.
Ce format est facile à générer et à intégrer, c’est pourquoi de nombreuses données publiques de l’Open Data
sont diffusées sous ce format.
Nous allons maintenant nous intéresser à la manière d’intégrer ce fichier de données au sein d’un environnement
Python.
On commence par découper le texte en lignes :
>>> planetes = open('planetes.csv', 'r')
>>> lignes = planetes.readlines()
>>> planetes.close()
et chaque ligne doit ensuite être découpée en colonnes. On utilise pour se faire la méthode split qui découpe
une chaîne de caractères en une liste de sous-chaînes, le séparateur de ces sous-chaînes étant indiqué en
paramètre. Dans le cas de notre fichier CSV cela donne :
2. À condition bien entendu que le texte ne soit pas démesurément grand.
3. Pour simplifier les explications, les en-têtes des colonnes ont été omis.
Jean-Pierre Becirspahic
11.4 informatique commune
>>> tab = []
>>> for chn in lignes:
... tab.append(chn.split(','))
[['Mercure', ' 2439', ' 3.7', ' 88\n'], ['Vénus', ' 6052', ' 8.9', ' 225\n'],
['Terre', ' 6378', ' 9.8', ' 365\n'], ['Mars', ' 3396', ' 3.7', ' 687\n']]
Il reste à convertir le deuxième et le quatrième terme de chacune de ces listes en un entier et le troisième en un
flottant :
>>> for lst in tab:
... lst[1] = int(lst[1])
... lst[2] = float(lst[2])
... lst[3] = int(lst[3])
En python, on obtient le code ascii d’un caractère à l’aide de la fonction ord ; à l’inverse, la fonction chr
retourne le caractère dont le code ascii est donné en paramètre :
>>> ord('A')
65
>>> chr(97)
'a'
0 1 2 3 4 5 6 7 8 9 a b c d e f
0
2 ! " # $ % & ’ ( ) * + , - . /
3 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
4 @ A B C D E F G H I J K L M N O
5 P Q R S T U V W X Y Z [ \ ] ˆ _
6 ‘ a b c d e f g h i j k l m n o
7 p q r s t u v w x y z { | } ˜
Malheureusement, ce codage simple est insuffisant pour pouvoir représenter la diversité des caractères des
langues autres que l’anglais, aussi le huitième bit a été utilisé pour ajouter au jeu de caractères ascii standard
128 autres caractères codés entre 128 = (80)16 et 255 = (ff)16 . Cependant, chaque langue ayant des besoins spéci-
fiques, ces extensions sont nombreuses et non compatibles entre elles : la norme latin-1 permet par exemple
d’encoder les langues d’Europe occidentale (en partie tout du moins, puisque le caractère œ manque pour le
français), la norme latin-2 pour les langues d’Europe centrale, etc. Pas moins de 16 variantes existent pour le
seul standard ISO 8859. Et encore ne s’agit-il ici que de représenter les caractères des langues alphabétiques,
car les écritures idéographiques comme le chinois nécessitent plusieurs milliers de caractères et ne peuvent
donc être codées sur un seul octet.
Bref, le seul intérêt résiduel de ces anciennes normes réside dans leur simplicité : chaque caractère typographique
est représenté par un seul octet, et une chaîne de caractères n’est rien d’autre qu’une séquence d’octets.
0 1 2 3 4 5 6 7 8 9 a b c d e f
8
a ¡ ¢ £ ¤ ¥ ¦ § ¨ © ª « ¬ ® ¯
b ˚ ± ² ³ ´ µ ¶ ˙ ¸ ¹ ° » ¼ ½ ¾ ¿
c À Á Â Ã Ä Å Æ Ç È É Ê Ë Ì Í Î Ï
d Ð Ñ Ò Ó Ô Õ Ö × Ø Ù Ú Û Ü Ý Þ ß
e à á â ã ä å æ ç è é ê ë ì í î ï
f ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ
Jean-Pierre Becirspahic
11.6 informatique commune
On peut observer que le caractère non ASCII "é" a été mal interprété à cause du mauvais encodage.
Les images en niveau de gris peuvent aussi être représentées par des matrices, mais cette fois chaque élément
détermine la luminance du pixel correspondant. Pour des raisons pratiques, la majorité des images en gris
utilisent un entier compris entre 0 (pour le noir) et 255 (pour le blanc), autrement dit par un entier non signé
codé sur 8 bits (un octet).
Enfin, les images en couleur peuvent être représentées par trois matrices, chacune déterminant la quantité
respective de rouge, de vert et de bleu qui constitue l’image. Ce modèle de couleur est appelé RGB (red-green-
blue) mais d’autres modèles existent, par exemple la quadrichromie (CMJN) en imprimerie.
Manipulation de fichiers 11.7
Les éléments de ces matrices sont des nombres entiers compris entre 0 et 255 (des entiers non signés sur 8 bits)
qui déterminent l’intensité de la couleur de la matrice pour le pixel correspondant. Le modèle RGB permet
donc de représenter 2563 = 224 = 16 777 216 couleurs différentes en théorie.
Figure 5 – Une image en couleur. Les trois autres images ont été obtenues en ne conservant qu’une des trois
composantes RGB qui la composent.
Attention, la fonction imrerad convertit une image m × n en niveau de gris en un tableau m × n dont les valeurs
sont non pas au type uint8 comme on pourrait s’y attendre (entiers non signés sur 8 bits) mais au type float32
(flottants codés sur 32 bits) et s’échelonnent entre 0. (le noir) et 1. (le blanc).
De même, une image en couleur de taille m × n sera transformée en un tableau tri-dimensionnel m × n × 3,
chaque pixel étant associé à un triplet RGB au type float32.
À titre d’exemple, l’image précédente a été produite à l’aide du script :
Jean-Pierre Becirspahic
11.8 informatique commune
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
img = mpimg.imread('picasso.png')
rouge = np.zeros_like(img)
vert = np.zeros_like(img)
bleu = np.zeros_like(img)
for i in range(img.shape[0]):
for j in range(img.shape[1]):
rouge[i, j, 0] = img[i, j, 0]
vert[i, j, 1] = img[i, j, 1]
bleu[i, j, 2] = img[i, j, 2]
plt.subplot(2, 2, 1)
plt.axis('off')
plt.imshow(img)
plt.subplot(2, 2, 2)
plt.axis('off')
plt.imshow(rouge)
plt.subplot(2, 2, 3)
plt.axis('off')
plt.imshow(vert)
plt.subplot(2, 2, 4)
plt.axis('off')
plt.imshow(bleu)
plt.show()
informatique commune
Chapitre 12
Bases de données
relationnelles
Introduction
Au sens premier du terme, l’informatique est la science du traitement automatique de l’information ; à ce titre,
la structuration des données en est un élément essentiel. Jusqu’à présent, nous avons essentiellement manipulé
des tableaux, qui supposent l’existence d’un ordre permettant le classement de l’information. Mais un tel
classement présente des limites : plusieurs critères peuvent être également pertinents, et ranger ces données
dans un tableau exige d’en privilégier certains au détriments d’autres.
Notons que le problème de l’ordonnancement de l’information n’est pas nouveau et précède la naissance de
l’informatique de plusieurs siècles : encore aujourd’hui les bibliothèques publiques utilisent un système de
classification inventé au xixe siècle : la classification décimale de Dewey. Les documents sont répartis en 10
classes, chaque classe est divisée en 10 divisions, chaque division en 10 subdivisions, et ainsi de suite. Cette
classification permet depuis bientôt 150 ans de ranger nos bibliothèques, mais n’en présente pas moins de
nombreux défauts : l’information est hiérarchisée suivant des critères qui étaient pertinents au moment de
l’élaboration de ce système mais qui ne le sont plus forcément aujourd’hui mais surtout, si elle facilite le
travail du classificateur, elle ne contribue pas à faciliter la tâche du chercheur, à moins que ce dernier ne sache
très précisément à quelle discipline rattacher l’objet de sa recherche. A contrario, les logiciels de gestion des
livres numériques gèrent sans peine des milliers de références en autorisant des recherches multi-critères
(et multi-bibliothèques) sans qu’il soit nécessaire pour l’utilisateur de connaître la structuration interne des
données.
Ces outils informatiques utilisent tous des bases de données relationnelles (BDR) qui offrent un moyen d’organiser
efficacement les données et de les manipuler grâce à des requêtes. Schématiquement, une base de données est
un ensemble de tables contenant des données reliées entre elles par des relations ; on y extrait de l’information
par le biais de requêtes exprimées dans un langage spécifique.
• Architecture trois-tiers
Sur un réseau informatique, des informations sont en permanence échangées entre deux machines, un logiciel
assurant le traitement des informations sur chacune d’entre elles. On distingue le logiciel client installé sur la
machine qui envoie des requêtes du logiciel serveur installé sur la machine qui traite les requêtes. Par extension,
les machines sont également désignées par les noms de client et serveur. Ce mode de communication est appelé
architecture à deux niveaux. C’est l’une des architectures client-serveur possibles.
L’architecture trois-tiers 1 est une architecture client-serveur qui ajoute un niveau supplémentaire dans l’envi-
ronnement précédemment décrit. Un serveur de données transmet les informations à un serveur d’application
qui, à son tour, transmet les informations traitées vers un client. Ce modèle d’architecture présente plusieurs
avantages :
– meilleure prise en compte de l’hétérogénéité des plates-formes ;
– amélioration de la sécurité des données en supprimant le lien entre le client et les données ;
– meilleure répartition des tâches entre les différentes couches logicielles.
http://info-llg.fr
12.2 informatique commune
réponse réponse
requête requête
BDR BDR
Client Serveur Client Serveur Serveur
d’application
Considérons à titre d’exemple la base de données mondial que nous allons utiliser pour illustrer ce cours. Il
s’agit d’une BDR qui compile un certain nombre de données géographiques et qui est gérée par l’université
de Göttingen. Il est possible d’interagir avec elle en utilisant un formulaire que l’on trouve à l’adresse :
http://www.semwebtech.org/sqlfrontend/
Nous sommes en présence d’une architecture trois tiers : la première couche (le client) est représentée en HTML
pour être exploitée par un navigateur web et sert d’interface entre l’homme et la machine. La seconde couche
(le serveur d’application) est un serveur web qui reçoit des données textuelles de la part du client, les transmet
sous la forme de requêtes SQL au serveur de la base puis actualise la page web du client pour y intégrer la
réponse du serveur. Enfin, la troisième couche est un SGBD, ici Oracle Database, qui gère la base de données
et répond aux sollicitations du serveur d’application.
Bases de données relationnelles 12.3
• Python et sql
SQLite est un autre SGBD qui présente l’avantage d’être présent dans la bibliothèque standard de Python.
Cela signifie que vous pouvez écrire en Python une application contenant son propre SGBD intégré à l’aide du
module sqlite3. Dans ce cas, il ne s’agit plus à proprement parlé d’une interface client-serveur puisque les
données sont intégralement stockées dans un fichier indépendant. Vous trouverez en annexe un script Python
rudimentaire mais suffisant pour pouvoir interagir avec une base de donnée enregistrée sur votre disque dur.
Enfin, on notera qu’il existe quelques différences entre les dialectes SQL utilisés par Oracle et par SQLite. Ces
différences seront indiquées dans ce document mais de toute façon, elles ne concernent que des notions hors
programme.
Remarque. Durant ce cours on pourra remarquer des différences entre les réponses fournies par la base
de données suivant que l’on interroge la base en ligne ou celle enregistrée sous la forme d’un fichier sur le
disque dur. Ces différences s’expliquent bien entendu par le fait que la première est mise à jour régulièrement,
contrairement à la seconde.
1. Le langage SQL
1.1 Relations
Nous l’avons dit, une base de données est un ensemble de tables 2 que l’on peut représenter sous la forme de
tableaux bi-dimensionnels. Par exemple, la base de données mondial contient (entre autre) une table nommée
country 3 qui possède six attributs :
Name Code Capital Province Area Population
Chaque attribut est un objet typé appelé ici domaine. Par exemple le domaine des quatre premiers attributs est
une chaîne de caractères, le domaine du cinquième attribut est un nombre flottant et le domaine de l’attribut
Population un entier.
Les attributs désignent les éléments de chacune des colonnes du tableau qui représente la relation ; les lignes en
forment les enregistrements : chacun d’eux est un n-uplet dont les éléments appartiennent à chaque colonne
de la table. En général, une table contient un grand nombre d’enregistrements, et le client de la BDR connaît
uniquement les attributs et leurs domaines respectifs (ce qu’on appelle le schéma de la relation) lorsqu’il
interagit avec elle.
• Clé primaire
En général, une base de données contient plusieurs tables et l’on peut souhaiter croiser les données présentes
dans plusieurs d’entre elles (nous verrons cela plus loin). Pour cela il est nécessaire de pouvoir identifier par une
caractérisation unique chaque enregistrement d’une table ; c’est le rôle de la clé primaire. En général constituée
d’un attribut (mais ce n’est pas une règle, certaines clé primaires peuvent être composées de plusieurs attributs),
elle garantit que deux enregistrements distincts ont deux clés primaires distinctes.
Dans le cas de la table country, la clé primaire est l’attribut Code ; il est fréquent que parmi les attributs d’une
table on trouve un identifiant, en général dénué de signification, dont le seul rôle est de jouer le rôle de clé
primaire, comme c’est le cas ici.
2. Ou de relations, les deux termes étant synonymes dans ce contexte.
3. Notons que les enregistrements de cette table ne sont pas tous des pays mais plus généralement des entités politiques, ce qui explique
par exemple que l’île de la Réunion soit une entrée distincte de la France.
http://info-llg.fr
12.4 informatique commune
Les mots-clés SELECT ... FROM réalisent l’interrogation de la table. Dans le cas de l’exemple ci-dessus on ne liste
qu’un seul des attributs de la table, pour en avoir plusieurs on sépare les attributs par une virgule ; pour les
avoir tous on les désigne par une étoile. Par exemple, les deux requêtes qui suivent donnent pour la première le
nom de chacun des pays ainsi que leurs capitales, pour la seconde l’intégralité des données de la table :
Le mot-clé WHERE filtre les données qui répondent à un critère de sélection. Par exemple, pour connaître le
nom de la capitale du Botswana on écrira :
Il se peut que certains attributs d’un enregistrement soient manquants ; dans ce cas la valeur de cet attribut est
NULL. Par exemple, dans la table country un territoire ne possède pas de capitale ; pour le connaître on produit
la requête :
Différentes clauses permettent de formuler des requêtes plus élaborées ; la figure 5 rassemble les instructions
les plus fréquentes.
Exercice 1 Rédiger une requête SQL pour obtenir :
a) la liste des pays dont la population excède 60 000 000 d’habitants ;
b) la même liste triée par ordre alphabétique ;
c) la liste des pays et de leurs populations respectives, triée par ordre décroissant de population ;
d) le nom des dix pays ayant la plus petite superficie ;
e) le nom des dix suivants.
Bases de données relationnelles 12.5
1.3 Jointures
L’intérêt d’une base de données réside en particulier dans la possibilité de croiser des informations présentes
dans plusieurs tables par l’intermédiaire d’une jointure. Dans la base de données qui nous occupe on trouve
une table nommée encompasses qui possède trois attributs :
Country Continent Percentage
Le premier attribut d’un enregistrement est le code du pays, le deuxième le nom du continent et le dernier la
portion du pays présente sur ce continent. La clé primaire de cette table est le couple (Country, Continent), et
la valeur du troisième argument ne peut pas être nulle.
Cette seconde table possède un attribut en commun avec la première table : l’attribut Country de la table
encompasses est identique à l’attribut Code de la table country et va nous permettre par son intermédiaire de
croiser les informations de ces deux tables.
Par exemple, pour connaître la liste des pays dont une fraction au moins est en Europe on écrira la requête :
SELECT c o u n t r y . name
FROM c o u n t r y JOIN encompasses
ON c o u n t r y . code = encompasses . c o u n t r y
WHERE encompasses . c o n t i n e n t = ' Europe '
Les mots-clés JOIN ... ON créent une table intermédiaire formée du produit cartésien des deux tables et applique
ensuite la requête sur la nouvelle relation.
Remarque. L’interrogation de plusieurs tables simultanément rend nécessaire le préfixage de l’attribut par
le nom de la table pour le cas où certaines d’entres-elles auraient des noms d’attributs en commun. On peut
alléger cette syntaxe à l’aide d’alias pour la rendre plus compacte. Par exemple, la requête précédente peut
s’écrire plus succinctement :
SELECT c . name
FROM c o u n t r y c JOIN encompasses e
ON c . code = e . c o u n t r y
WHERE e . c o n t i n e n t = ' Europe '
Ainsi, réaliser cette jointure revient à créer une table (virtuelle) nommée :
Exercice 2 Rédiger une requête SQL pour obtenir :
a) le nom des pays qui sont à cheval sur plusieurs continents ;
b) les pays du contient américain qui comptent moins de 10 habitants par km2 .
c) Dans la base de données figure une table nommée city qui possède les attributs suivants :
http://info-llg.fr
12.6 informatique commune
Enfin, on notera que c’est à l’aide du mot-clé HAVING que l’on peut imposer des conditions sur un groupe. Par
exemple, pour obtenir la liste des continents dont la population totale dépasse le milliard d’habitants on écrira :
SELECT e . c o n t i n e n t , SUM ( c . p o p u l a t i o n )
FROM c o u n t r y c JOIN encompasses e ON c . code = e . c o u n t r y
GROUP BY e . c o n t i n e n t
HAVING SUM ( c . p o p u l a t i o n ) > 1000000000
Exercice 3 La table language possède les attributs suivants :
Country Name Percentage
L’attribut Country est le code du pays, Name le nom d’une langue parlée dans celui-ci, et Percentage la
proportion d’habitants dont c’est la langue maternelle 4 .
a) Donner la liste ordonnée des dix langues parlées dans le plus de pays différents.
b) Quelles sont les langues parlées dans exactement six pays ? Et de quels pays s’agit-t-il ?
c) Quelles sont les langues parlées par moins de 30 000 personnes dans le monde ?
d) Quelles sont les cinq langues les plus parlées en Afrique ? On précisera pour chacune d’elles le nombre de
personnes qui la parlent.
1.5 Sous-requêtes
Notons pour finir qu’il est possible d’imbriquer une requête dans une clause SELECT, ou (le plus souvent) au sein
d’un filtre WHERE ou HAVING, la sous-requête devant simplement être encadrée par une paire de parenthèses.
Supposons par exemple que l’on veuille déterminer les pays dont la densité de population est supérieure à la
moyenne. Il y a clairement deux calculs à effectuer : d’abord le calcul de la moyenne, puis la détermination des
pays dont la densité est supérieure à cette moyenne. Ceci peut se traduire en une seule requête SQL :
SELECT name FROM c o u n t r y
WHERE p o p u l a t i o n / a r e a > ( SELECT AVG ( p o p u l a t i o n / a r e a ) FROM c o u n t r y )
Exercice 4 Dans la BDR mondial se trouve une table economy qui possède les attributs suivants : Country
(le code du pays), GDP (le PIB, en millions de dollars), agriculture (la part de l’agriculture dans le PIB, en
pourcentage), Service (la part des services dans le PIB), Industry (la part de l’industrie dans le PIB), Inflation
(le taux d’inflation) et Unemployment (le taux de chômage).
a) Déterminer les pays majoritairement agricoles dont le taux de chômage est inférieur à la moyenne mondiale.
b) Déterminer pour chaque continent le pays au taux d’inflation le plus faible parmi les pays majoritairement
industriels.
4. Attention, ces données ne sont pas disponibles pour tous les pays.
Bases de données relationnelles 12.7
2. Algèbre relationnelle
SQL est le langage de prédilection pour interagir avec une BDR, mais ce l’est pas le seul. L’algèbre relationnelle
fournit un cadre théorique indépendant du langage, proche de la théorie des ensembles. Les opérations sur les
BDR y sont définies de manière formelle et permettent de les composer efficacement entre-elles. La formulation
abstraite dans le cadre de l’algèbre relationnelle permet d’obtenir des requêtes non seulement correctes mais
aussi et surtout efficaces.
De même que l’algorithmique permet de formuler un problème informatique indépendamment d’un langage
de programmation particulier, l’algèbre relationnelle définit un cadre formel dans lequel exprimer des requêtes
et des relations. Elle s’appuie très largement sur la théorie des ensembles et propose un ensemble d’opérations
formelles qui permettent de construire de nouvelles relations à partir de relations existantes.
SELECT * FROM t1 UNION SELECT * FROM t2 enregistrements présents dans l’une des deux tables
t1 ou t2 (sans répétition)
SELECT * FROM t1 INTERSECT SELECT * FROM t2 enregistrements présents dans les deux tables t1 et t2
SELECT * FROM t1 EXCEPT SELECT * FROM t2 enregistrements présents dans t1 mais pas dans t2
Pour être réalisées, ces opérations doivent agir sur des relations ayant le même schéma relationnel :
R1 R2
A B C A B C
a1 b1 c1 a1 b1 c1
a2 b2 c2 a3 b3 c3
R1 ∪ R2 R1 ∩ R2 R1 − R2
A B C A B C A B C
a1 b1 c1 a1 b1 c1 a2 b2 c2
a2 b2 c2
a3 b3 c3
R π(A,B) (R)
A B C A B
a1 b1 c1 a1 b1
a2 b2 c2 a2 b2
a1 b1 c3
http://info-llg.fr
12.8 informatique commune
Sélection
La sélection permet d’extraire les enregistrements d’une relation R qui satisfont une expression logique E. On la
note :
σE (R)
En SQL la sélection se traduit par la requête :
SELECT * FROM table WHERE expression_logique
R σE (R)
A B C A B C
a1 b1 c1 a2 b2 c2
a2 b2 c2 a4 b4 c4
a3 b3 c3
a4 b4 c4
Renommage
Le renommage permet la modification du nom d’un ou plusieurs attributs d’une relation. Renommer l’attribut a
en l’attribut b dans la relation R s’écrit :
ρa←b (R)
En SQL le renommage se traduit par la requête :
ALTER TABLE table RENAME COLUMN a TO b
Attention, le renommage n’est pas possible avec SQLite (seule le renommage d’une table est possible).
R ρa←d (R)
A B C D B C
a1 b1 c1 a1 b1 c1
a2 b2 c2 a2 b2 c2
a3 b3 c3 a3 b3 c3
Jointure
La jointure est une opération qui porte sur deux relations R1 et R2 et retourne une relation qui comporte les
enregistrements des deux premières relations qui satisfont une contrainte logique E. Cette nouvelle relation se
note :
R1 ZE R2
En SQL on réalise une jointure par la requête :
SELECT * FROM table1 JOIN table2 ON expression_logique
R1 R2 R1 ZA=D R2
A B C D E A B C E
a1 b1 c1 a1 e1 a1 b1 c1 e1
a2 b2 c2 a2 e2 a2 b2 c2 e2
a3 b3 c3
R1 × R2
R1 R2 R1 × R2
A B C D E A B C D E
a1 b1 c1 d1 e1 a1 b1 c1 d1 e1
a2 b2 c2 d2 e2 a1 b1 c1 d2 e2
a2 b2 c2 d1 e1
a2 b2 c2 d2 e2
Bases de données relationnelles 12.9
Effectuer le produit cartésien de deux tables de grandes tailles n’est pas une opération toujours pertinente, mais
il constitue une opération de base pour définir d’autres opérations plus complexes. Ainsi, la jointure est un
produit cartésien suivi d’une sélection :
R1 ZE R2 = σE (R1 × R2 ).
Enfin, la division cartésienne est encore une opération qui produit une relation à partir de deux relations R1 et
R2 vérifiant R2 ⊂ R1 . La relation obtenue possède tous les attributs de R1 que ne possède pas R2 ; on la note :
R1 ÷ R2
R1 R2 R1 ÷ R2
A B C D C D A B
a1 b1 c1 d1 c1 d1 a1 b 1
a1 b1 c2 d2 c2 d2 a3 b 3
a2 b2 c3 d3
a3 b3 c1 d1
a3 b3 c2 d2
Exercice 6 Montrer que si X est un ensemble d’attributs de R et A un attribut élément de X alors
σA=a (πX (R)) = πX (σA=a (R)).
http://info-llg.fr
12.10 informatique commune
base = sql.connect("mondial.sq3")
La méthode cursor appliquée à la connexion que nous venons de créer crée un intermédiaire entre l’interface
et la BDR 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 vers la base de données. Son utilisation permet donc
d’annuler si nécessaire plusieurs opérations qui se seraient révélées inadéquates sans que la base de données
n’en soit affectée.
cur = base.cursor()
Une fois le curseur créé, la méthode execute permet de transmettre des requêtes rédigées en SQL sous forme
de chaîne de caractères :
cur.execute("ici on écrit une requête en langage SQL")
Enfin, si des modifications ont été effectuées sur la BDR, il faut appliquer la méthode commit à la connexion
créée pour qu’elles deviennent définitives. On peut ensuite refermer le curseur et la connexion :
cur.close()
base.close()
Voici enfin un script permettant un dialogue interactif avec la base de données mondial.sq3 utilisée dans ce
document :
import sqlite3
while True:
requete = input("Veuillez entrer une requête SQL (ou 'stop' pour terminer) :")
if requete == 'stop':
break
try:
cur.execute(requete)
except:
print('*** Requête SQL incorrecte ***')
else:
for enreg in cur:
print(enreg)
print()
cur.close()
base.close()
informatique commune
Chapitre 12
Corrigé des exercices
Exercice 1
a)
b)
c)
d) Avec SQLite :
Avec Oracle :
SELECT name FROM c o u n t r y
ORDER BY a r e a ASC FETCH FIRST 10 ROWS ONLY
e) Avec SQLite :
Avec Oracle :
SELECT name FROM c o u n t r y
ORDER BY a r e a ASC OFFSET 10 ROWS FETCH FIRST 10 ROWS ONLY
Exercice 2
a)
b)
SELECT c . name
FROM c o u n t r y c JOIN encompasses e ON c . code = e . c o u n t r y
WHERE e . c o n t i n e n t = ' America ' AND c . p o p u l a t i o n / c . a r e a < 10
http://info-llg.fr
12.2 informatique commune
c)
SELECT c . name , c . c a p i t a l
FROM c o u n t r y c JOIN c i t y v ON c . code = v . c o u n t r y
JOIN encompasses e ON c . code = e . c o u n t r y
WHERE e . c o n t i n e n t = ' Europe ' AND v . name = c . c a p i t a l AND v . l a t i t u d e > 60
Exercice 3
a) Avec SQLite :
Avec Oracle :
SELECT name , COUNT ( * ) c FROM l a n g u a g e
GROUP BY name
ORDER BY c DESC FETCH FIRST 10 ROWS ONLY
b)
c)
SELECT l . name
FROM l a n g u a g e l JOIN c o u n t r y c ON l . c o u n t r y = c . code
GROUP BY l . name
HAVING SUM ( c . p o p u l a t i o n * l . p e r c e n t a g e / 1 0 0 ) < 30000
d) Avec SQLite :
Avec Oracle :
SELECT l . name , FLOOR ( SUM ( c . p o p u l a t i o n * l . p e r c e n t a g e / 1 0 0 ) ) s
FROM l a n g u a g e l JOIN c o u n t r y c ON l . c o u n t r y = c . code
JOIN encompasses e ON c . code = e . c o u n t r y
WHERE e . c o n t i n e n t = ' A f r i c a '
GROUP BY l . name
ORDER BY s DESC
FETCH FIRST 5 ROWS ONLY
Corrigé des exercices 12.3
Exercice 4
a)
SELECT c . name
FROM c o u n t r y c JOIN economy e ON c . code = e . c o u n t r y
WHERE e . a g r i c u l t u r e > e . s e r v i c e AND e . a g r i c u l t u r e > e . i n d u s t r y
AND e . unemployment < ( SELECT AVG ( unemployment ) FROM economy )
b)
SELECT en . c o n t i n e n t , c . name
FROM c o u n t r y c JOIN economy e ON c . code = e . c o u n t r y
JOIN encompasses en ON en . c o u n t r y = c . code
WHERE e . i n d u s t r y > e . a g r i c u l t u r e AND e . i n d u s t r y > e . s e r v i c e
AND ( en . c o n t i n e n t , e . i n f l a t i o n ) IN
( SELECT en . c o n t i n e n t , MIN ( e . i n f l a t i o n )
FROM economy e JOIN encompasses en ON e . c o u n t r y = en . c o u n t r y
WHERE e . i n d u s t r y > e . a g r i c u l t u r e AND e . i n d u s t r y > e . s e r v i c e
GROUP BY en . c o n t i n e n t )
Exercice 5 La première expression décrit les villes de plus de 10 000 habitants situées au delà du cercle
polaire arctique ; la seconde le nom des pays qui possèdent au moins une ville située entre l’équateur et le
tropique du Capricorne.
Exercice 6 Notons (A1 , . . . , An ) les attributs de R, et X = (Ai1 , . . . , Aip ) avec Aik = A.
πX (R) sélectionne les enregistrements de E de la forme (Ai1 , . . . , Aip ) et σA=a (πX (R)) ceux de la forme :
σA=a (R) sélectionne les enregistrements de la forme (A1 , . . . , Aik −1 , a, Aik +1 , . . . , An ) et πX (σA=a (R)) ceux de la
forme :
(Ai1 , . . . , Aik−1 , a, Aik+1 , . . . , Aip ).
Il s’agit bien de la même chose.
En revanche, si A < X, σA=a (πX (R)) renvoie l’ensemble vide alors que πX (σA=a (R)) sélectionne les enregistrements
(Ai1 , . . . , Aip ) pour lesquels il existe un enregistrement de la forme (A1 , . . . , a, . . . , An ).
Exercice 7 R1 possède r1 attributs ; les r2 derniers attributs sont les attributs de R2 .
S1 est constitué de enregistrements obtenus en sélectionnant les r1 − r2 premiers attributs d’un enregistrement
de R1 .
S1 ×R2 −R1 est donc l’ensemble des enregistrements qui n’appartiennent pas à R1 mais que l’on forme en prenant
r1 − r2 premières composantes d’un enregistrement de R1 suivies de r2 composantes d’un enregistrement de R2 .
S2 est donc l’ensemble des enregistrements x constitués des r1 − r2 premiers attributs d’un enregistrement de
R1 mais tels que pour tout enregistrement y de R2 , xy n’appartient pas à R1 .
On en déduit que S1 − S2 représente R1 ÷ R2 .
http://info-llg.fr