Vous êtes sur la page 1sur 129

informatique commune

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.

1. Principaux composants d’un ordinateur


Un ordinateur peut être décrit comme une machine de traitement de l’information 1 obéissant à des programmes
constitués de suites d’opérations arithmétiques et logiques : il est capable d’acquérir de l’information, de la
stocker, de la transformer et de la restituer sous une autre forme.
– L’acquisition de l’information se fait par l’intermédiaire de périphériques d’entrées que sont le clavier, la
souris, le micro, la webcam, le scanner, l’écran tactile, . . .
– Le stockage de l’information se fait dans ce qu’on appelle généralement la mémoire d’un ordinateur. On
peut distinguer la mémoire de masse (disque dur, clé USB, etc.) destinée à un stockage persistant même
en absence d’alimentation électrique, de la mémoire vive (la RAM 2 ) utilisée par le processeur pour traiter
les données qui nécessite d’être alimentée électriquement.
– La transformation de l’information est le rôle du processeur (le CPU 3 ), composé de deux éléments :
l’unité de commande, responsable de la lecture en mémoire et du décodage des instructions et l’unité de
traitement 4 qui exécute les instructions qui manipulent les données.
1. d’ailleurs, le mot informatique vient de la contraction des mots information et automatique.
2. pour Ramdom Access Memory, ou mémoire à accès direct en français.
3. de l’anglais Central Processing Unit.
4. aussi appelée Unité Arithmétique et Logique.

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.

1.1 Le modèle de von Neumann


Les différents éléments décrits ci-dessus s’articulent suivant l’architecture conçue par Von Neuman et son
équipe (voir la figure 1).

périphériques de sortie

programmes
unité de commande

processeur mémoire

unité de traitement
données

périphériques d’entrée

Figure 1 – L’architecture d’un ordinateur selon le modèle de von Neuman.

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.

1.2 La mémoire principale


Une information traitée par un ordinateur peut être de différents types (texte, nombre, image, son, . . .) mais
elle est toujours représentée et manipulée par l’ordinateur sous forme binaire, c’est à dire par une suite de 0 et
de 1 appelés bits (pour binary digits, chiffres binaires en anglais). Par la suite, il faudra prendre garde à bien
distinguer un objet de sa représentation machine : deux objets distincts, disons un nombre et un caractère,
peuvent avoir la même représentation machine, mais vont différer par ce qu’on appelle leur type dans un
langage de programmation, voire tout simplement par le contexte dans lequel ils sont utilisés.
Éléments d’architecture des ordinateurs 1.3

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

vitesse RAM capacité

mémoire centrale

mémoire de masse

Figure 2 – Les différents types de mémoire dans un ordinateur.

– 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

préfixe valeur théorique mésusage préfixe valeur théorique


1 k (kilo) 103 = 1 000 210 = 1 024 1 ki (kibi) 210 = 1 024
1 M (méga) 106 = 1 000 000 220 = 1 048 576 1 Mi (mébi) 220 = 1 048 576
1 G (giga) 109 = 1 000 000 000 230 = 1 073 741 824 1 Gi (gibi) 230 = 1 073 741 824

Figure 3 – Multiples usuels en informatique.

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.

temps d’accès (ordre de grandeur)


registre 1 ns
cache 5 ns
mémoire centrale 10 ns
mémoire de masse 10 ms

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

Registre d’entrée Accumulateur

instruction UAL

Figure 5 – Schéma d’une unité de traitement.

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.

Un exemple de programme en assembleur


Pour illustrer le fonctionnement d’un processeur, nous allons imaginer une machine dont les mots-mémoire
sont de 16 bits. Chaque instruction comporte un code d’opération sur 5 bits spécifiant l’opération à effectuer et
une adresse sur 11 bits qui est celle de l’opérande de cette instruction. Ainsi il peut y avoir 25 = 32 instructions
différentes et 211 = 2048 emplacements en mémoire.
Nous allons écrire un programme en assembleur utilisant les trois instructions suivantes :
assembleur machine rôle
LOAD X 01010 charge dans l’accumulateur le contenu de l’emplacement d’adresse X
STO X 01110 range le contenu de l’accumulateur dans l’emplacement d’adresse X
ADD X 10111 charge dans le registre le contenu de l’emplacement d’adresse X
et l’ajoute à l’accumulateur
Le programme que nous souhaitons réaliser consiste à réaliser l’opération : c ← a + b : ranger à l’adresse c la
valeur de la somme des entiers stockés aux adresses a et b. Avec notre modèle les adresses sont décrites sur
11 bits ; choisissons arbitrairement a = 10000000011, b = 01110000110, c = 11001100110. En assembleur le
programme va ressembler à ceci :
LOAD A
ADD B
STO C
En langage machine, ce programme sera stocké dans trois mots-machine consécutifs quelque part dans la
mémoire :

01010 10000000011
programme 10111 01110000110
01110 11001100110

01110000110 (b) → 0010001011010101

10000000011 (a) → 0001101100110001

11001100110 (c) → 0011111000000110 = 0001101100110001 + 0010001011010101

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.

1.4 Langages de programmation


• Fortran
Fortran () est le premier langage de haut niveau ayant connu une large diffusion. Il était destiné au calcul
scientifique (la principale application à l’époque), et son usage s’est vite répandu auprès des scientifiques et des
ingénieurs. L’importance de sa bibliothèque de fonctions mathématiques qui s’est constituée au fil du temps est

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

Figure 6 – Compilation et interprétation.

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éé.

.py compilateur .pyc interprète

script bytecode
machine virtuelle

Figure 7 – Interprétation d’un script Python.

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.

2.2 Session utilisateur


Les systèmes d’exploitation actuels des ordinateurs (et en particulier Unix) sont multi-tâches et multi-utilisateurs :
ils sont capables de partager des ressources selon une hiérarchie de droits d’accès. Chaque utilisateur se voit
attribuer un compte comportant :
– un identifiant associé à un mot de passe ;
– un répertoire d’accueil personnel destiné à héberger tous les sous-répertoires et fichiers qui lui appar-
tiennent.
L’utilisateur commande au système d’exploitation par l’intermédiaire d’une interface. Jadis purement textuelle
(l’utilisateur n’avait d’autre choix que d’entrer ses commandes par l’intermédiaire de son clavier), le développe-
ment de l’informatique grand public a conduit à l’apparition d’interfaces graphiques se manipulant à l’aide
d’un pointeur dirigé par une souris ou par un doigt (écran ou pavé tactile). Leur avantage est de permettre une
utilisation plus intuitive du logiciel d’exploitation, mais une interface par ligne de commande reste plus efficace
pour un utilisateur aguerri. C’est pourquoi les systèmes d’exploitation modernes continuent de proposer des
interprètes de commandes textuelles : c’est le Terminal sous OS X ou Linux et PowerShell sous windows.

2.3 Hiérarchie des fichiers


Quel que soit le système d’exploitation, l’utilisateur interagit avec une machine virtuelle bien différente de la
machine réelle. Dans cette machine virtuelle, l’ensemble des fichiers (applications ou données) est structuré
sous forme arborescente : les feuilles de cet arbre sont les fichiers, les nœuds étant appelés des répertoires. On
est bien loin de la machine réelle pour laquelle un fichier n’est pas nécessairement constitué d’emplacements
consécutifs en mémoire (c’est le phénomène de fragmentation).
Sous Unix par exemple, la racine de cet arbre est désignée par / ; c’est un répertoire qui contient lui-même
d’autres répertoires : bin pour les exécutables, dev pour les périphériques, etc pour le système, home pour les
utilisateurs, tmp pour les fichiers temporaires, usr pour les outils, etc. et ces répertoires contiennent eux-mêmes
d’autres répertoires et fichiers (voir illustration figure 8).
Au sein de cette hiérarchie, chaque utilisateur possède une branche de l’arbre constitué d’un répertoire d’accueil
qui contient les sous-répertoires et fichiers qui lui appartiennent. Dans l’exemple représenté ici, Alice et Bob
possèdent tous deux un répertoire personnel rangé dans le répertoire home.

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.

6. Il en existe de multiples versions.


Éléments d’architecture des ordinateurs 1.9

bin dev etc home lib tmp usr

ls pwd alice bob bin

doc1 doc2 travail loisir man cd

doc1

Figure 8 – un exemple partiel d’arborescence Unix

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

2.4 Droits d’accès


À chaque fichier est associé un certain nombre d’attributs précisant sa nature (fichier ordinaire ou exécutable)
ainsi que les droits d’accès qui sont attribués au propriétaire et aux autres utilisateurs. Ces droits d’accès peuvent
être de trois types : droit en lecture (r – read), droit en écriture (w – write), droit d’exécution (x – execute). Le
droit en lecture permet de lire ce fichier mais pas de le modifier (il faut pour cela bénéficier du droit d’écriture),
le droit d’exécution permet d’utiliser un programme.
Pour chaque fichier il y a trois classes d’utilisateurs : user (le propriétaire du fichier), group (le groupe
auquel appartient le fichier), other (tous les autres). Ainsi, les droits d’accès d’un fichier sont décrits par une
succession de neuf symboles ; par exemple, un fichier dont le droit d’accès est rwxr−xr−− est accessible en
lecture/écriture/exécution (rwx) pour le propriétaire, en lecture/exécution pour son groupe (r−x) et en lecture
seule pour les autres (r−−).
Chacun de ces triplets peut aussi être décrit par un nombre binaire compris entre 000 (aucun droit) et 111
(tous les droits). Converti en base 10, on obtient un nombre compris entre 0 et 7 pour décrire les huit cas
de figure possible. Les droits d’accès du fichier précédent peuvent donc être décrits par le nombre 754 car
7 = (111)2 = rwx, 5 = (101)2 = r−x, 4 = (100)2 = r−−.
Dans le terminal la fonction chmod permet de changer les droits d’accès. Par exemple, si Alice veut attribuer le
droit d’accès r−xr−x−−x au fichier doc1 elle écrira :

chmod 551 ~/doc1

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 :

chmod 733 ~/public

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

En revanche, il ne pourra pas visualiser le contenu de ce répertoire en écrivant :

ls ../alice/public

car ses droits ne sont pas suffisants.


informatique commune
Chapitre 2
Introduction à Python et à
son environnement

1. Écosystème et environnement de travail


Python est un langage de programmation polyvalent et modulaire, utilisé dans de très nombreux domaines,
scientifiques ou non. C’est pourquoi il existe de nombreuses distributions de python. Au lycée Louis-le-Grand
nous utilisons Pyzo, distribution gratuite et open-source livrée avec un environnement de développement
intégré simple à utiliser et avec tous les modules scientifiques dont nous aurons besoin cette année.

1.1 Installation du logiciel


La première chose à faire est de télécharger Pyzo à l’adresse suivante : http://www.pyzo.org/ et de l’installer.
Pyzo est disponible pour Windows, Linux et OSX (a priori si votre ordinateur n’est pas trop ancien vous aurez
besoin de la version 64 bits). Comme souvent, il est conseillé d’utiliser la dernière version disponible et de
vérifier de temps à autre si une nouvelle version n’est pas proposée au téléchargement.

1.2 L’environnement de travail


L’environnement de développement que vous venez d’installer est un ensemble d’outils pour programmeurs
conçus pour être utilisés au sein d’un éditeur interactif nommé IEP (Interactive Editor for Python). IEP consiste
avant tout en un éditeur et une interface système (un shell) ; les autres outils ne sont pas indispensables à nos
besoins pour l’instant très modestes.

• 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.

>>>

– S’il s’agit de l’interprète IPython, le shell aura cette forme :


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 PYSIDE.

Using IPython 2.4.1 −− An enhanced Interactive Python.


? −> Introduction and overview of IPython's features.
%quickref −> Quick reference.
help −> Python's own help system.
object? −> Details about 'object', use 'object??' for extra details.

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 :

>>> print('Hello world !') In [2]: print('Hello world !')


Hello world ! Hello world !

>>> 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.

Quelle différence entre retour et effet sur l’environnement ?


Pour tenter de mieux comprendre celle-ci, jetons un coup d’œil sur l’aide en ligne de la fonction print :
In [3]: help(print)
Help on built−in function print in module builtins:

print(...)
print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

Prints the values to a stream, or to sys.stdout by default.


Optional keyword arguments:
file: a file−like object (stream); defaults to the current sys.stdout.
sep: string inserted between values, default a space.
end: string appended after the last value, default a newline.
flush: whether to forcibly flush the stream.

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 [4]: print('Hello world !', file=open('essai.txt', 'w'))

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 [5]: 1 + 2 In [5]: print(1 + 2) In [5]: print(print(1 + 2))

In [6]: In [6]: In [6]:

In [5]: print(1) + 2 In [5]: print(1) + print(2)

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')

puis exécutons ce script. Voici ce que l’on obtient :

Jean-Pierre Becirspahic
2.4 informatique commune

In [7]: (executing lines 1 to 2 of "<tmp 1>")


une journée a une durée égale à 86400 secondes

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 :

print(1, 2, 3, sep='+', end='=')


print(6, 5, 4, sep='\n', end='*')

• 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.

2. Premiers pas en Python


Un aspect essentiel des langages de programmation est la notion de type : tout objet manipulé par Python en
possède un, qui caractérise la manière dont il est représenté en mémoire, et dont vont dépendre les fonctions
qu’on peut lui appliquer.
En effet, quand nous nous intéresserons plus tard à la représentation en mémoire de certains objets simples,
nous constaterons que deux objets de natures distinctes peuvent avoir la même représentation ; dans ce cas,
seuls leurs types respectifs vont permettre de les distinguer.

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

Et même une fonction possède un type : c’est aussi un objet Python.

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.

2.1 La représentation des nombres en Python


En mathématique nous connaissons plusieurs sorte de nombres : entiers naturels, entiers relatifs, nombres
rationnels, nombres réels, nombres complexes. En Python il existe de base trois types qui peuvent représenter
des nombres :
– le type int représente des nombres entiers ;
– le type float représente des nombres décimaux ;
– le type complex représente des nombres complexes.
(Un module additionnel permet de manipuler des nombres rationnels).
Bien entendu, cette représentation est forcément imparfaite : les ensembles de nombres mathématiques sont
de cardinal infini or seul un nombre fini d’entre eux peut être représenté en machine car la mémoire est en
quantité finie. Pour des raisons analogues les nombres réels dont la représentation décimale est infinie 2 ne
peuvent être représentés autrement que par une approximation.
Attention. En mathématique, un nombre entier est aussi un nombre réel et un nombre réel est aussi un nombre
complexe. Ce n’est pas le cas en informatique, chacun des trois types int, float et complex étant représenté d’une
façon spécifique en mémoire. par exemple, le nombre 2 peut être représenté :
– dans le type int sous la forme 2
– dans le type float sous la forme 2.0
– dans le type complex sous la forme 2 + 0j

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

Figure 1 – Les principales opérations sur les nombres.

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

Figure 2 – Quotient et reste de 23 par 3.

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 .

• Conversion explicite de type


Nous avons vu que si cela s’avère nécessaire, l’interprète de commande réalise la conversion implicite de type
int −→ float −→ complex, ce qui est somme toute naturel compte tenu des inclusions mathématiques Z ⊂ R ⊂ C.
Les conversions réciproques sont parfois possibles, mais le résultat n’est jamais garanti 4 . Le nom des fonctions
de conversion est simple à retenir : il s’agit tout simplement des noms des types qu’on souhaite obtenir. En voici
quelques exemples :
In [9]: int(2.3333) In [12]: complex(2)
Out[9]: 2 Out[12]: (2+0j)

In [10]: int(−2.3333) In [13]: float(9999999999999999)


Out[10]: −2 Out[13]: 1e+16

In [11]: float(25) In [14]: int(1e+16)


Out[11]: 25.0 Out[14]: 10000000000000000

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

• Fonctions mathématiques supplémentaires


Lorsqu’on utilise le langage Python de base, seul un nombre très limité de fonctions est disponible ; heureuse-
ment, il existe une multitude de modules additionnels que l’utilisateur peut ajouter en fonction de ses besoins
et qui lui fournissent des fonctions supplémentaires. Par exemple, les modules math ou numpy enrichissent le
corpus utilisable des fonctions mathématiques suivantes :
– les fonctions trigonométriques sin, cos, tan ainsi que leurs fonctions réciproques 5 arcsin, arccos, arctan
(les angles s’expriment par défaut en radians) ;
– les fonctions exponentielle exp et logarithme log ;
– la fonction racine carrée sqrt.
Sachez qu’il en existe de nombreuses d’autres ; si nécessaire, consulter l’aide en ligne pour les découvrir.

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.

Figure 3 – Les deux modes d’importation d’un module.

S’agissant de fonctions à valeurs numériques, le résultat retourné est le plus souvent un objet de type float ou
complex.

5. Vous en apprendrez les définitions cette année.

Jean-Pierre Becirspahic
2.8 informatique commune

In [18]: import numpy as np

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

In [21]: from math import exp

In [22]: exp(1) # la fonction exponentielle du module math


Out[22]: 2.718281828459045

In [23]: np.exp(1) # la fonction exponentielle du module numpy


Out[23]: 2.7182818284590451

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).

x < y retourne True si x < y, False sinon


x > y même chose avec la condition x > y
x <= y même chose avec la condition x 6 y
x >= y même chose avec la condition x > y
x == y même chose avec la condition x = y
x != y même chose avec la condition x , y

Figure 4 – Opérateurs à valeurs dans le type bool.

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

In [2]: (1 + 1 == 3) != ('Henri 4' > 'Louis−le−Grand')


Out[2]: False

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

In [1]: largeur = 12.45

In [2]: longueur = 42.18

In [3]: aire = longueur * largeur

In [4]: print("l'aire du rectangle est égale à", aire)


l'aire du rectangle est égale à 525.141

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

Figure 5 – Variables référencées en mémoire.

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

et regardons de nouveau l’identifiant de la variable :


In [7]: id(largeur)
Out[7]: 4577628442

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

Tableau de référencement 4577628442 : 15.7


Nom Adresse
aire 4577628496 12.45
largeur 4577628442
longueur 4577628352

4577628352 : 42.18

Figure 6 – Nouveau référencement d’une variable.

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).

a valeur1 b = a a valeur1 a = b a valeur1

b valeur2 b valeur2 b valeur2

Figure 7 – Une permutation incorrecte.

À 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

Dès lors, permuter leur contenu devient évident :


In [10]: a, b = b, a


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

Égalité de valeur, égalité physique


De ce mécanisme résulte l’existence de deux sortes d’égalités entre variables :
– on parle d’égalité de valeur lorsque deux variables a et b référencent la même valeur à deux emplacements
mémoires pouvant être différents ;
– on parle d’égalité physique lorsque deux variables a et b référencent le même emplacement mémoire (ce
qui implique bien entendu l’égalité de valeur).
Nous connaissons déjà l’opérateur qui teste l’égalité de valeur : il s’agit de l’opérateur == ; l’opérateur qui teste
l’égalité physique se nomme is.

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

Figure 8 – Différence entre égalité de valeur et égalité physique.

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.

2.4 Chaînes de caractères


Outre les types numériques, Python permet la manipulation de données alphanumériques, qu’on appelle des
chaînes de caractères, et qui correspondent au type str (string en anglais). On définit une chaîne de caractères en
encadrant celle-ci par des ', des ", voire des ''', ces caractères ne faisant pas partie de la chaîne. Le choix est
souvent imposé par le contenu : une chaîne contenant un guillemet simple sera encadré par des guillemets
doubles, et réciproquement. Par exemple :

In [1]: "aujourd'hui"
Out[1]: "aujourd'hui"

In [2]: 'et demain'


Out[2]: 'et demain'

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 :

In [3]: print("un passage\n à la ligne")


un passage
à la ligne

(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 [4]: chn = "Hello "

In [5]: chn += 'world !' # équivalent à chn = chn + 'world !'

In [6]: print(chn)
Hello world !

In [7]: chn * 3 # équivalent à chn + chn + chn


Out[7]: 'Hello world !Hello world !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 [8]: 123 + 1 # addition de deux entiers


Out[8]: 124

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

In [11]: '123' + str(1)


Out[11]: '1231'

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.

• Accès aux caractères individuels d’une chaîne


Python offre une méthode simple pour accéder aux caractères contenus dans une chaîne : chaque caractère
est accessible directement par son rang dans la chaîne en utilisant des crochets. Attention, comme souvent en
informatique, le premier caractère de la chaîne est indexé par 0 :
In [12]: ch = 'Louis−Le−Grand'

In [13]: ch[4]
Out[13]: 's'

In [14]: ch[0] + ch[6] + ch[9]


Out[14]: 'LLG'

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

In [15]: print(ch[−8] * 2 + ch[−5])


LLG

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.

1.1 Définition d’une fonction


On définit une fonction en python à l’aide du mot clé def. Il faut lui attribuer un nom, préciser la liste de ses
paramètres et enfin décrire les différentes instructions à réaliser. La syntaxe générale est la suivante :

def nomdelafcn(liste de paramètres):


bloc ...........................
d'instructions .................
à réaliser .....................

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

In [1]: print("Bonjour\ntout le monde")


Bonjour
tout le monde

In [2]: len("Bonjour\ntout le monde")


Out[2]: 21

On notera que bien que la procédure print retourne la valeur None, celle-ci est ignorée par l’interprète de
commande.

• Arguments d’une fonction


Une fonction peut posséder un ou plusieurs arguments (ou paramètres, c’est la même chose) séparés par une
virgule 2 . Par exemple, pour définir la norme euclidienne d’un vecteur de coordonnées (x, y) on écrira :

from numpy import sqrt

def norme(x, y):


return sqrt(x**2 + y**2)

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 :

def norme(x, y, k):


return (x**k + y**k)**(1/k)

Avec cette nouvelle définition, on a :


In [2]: norme(3, 4, 2)
Out[2]: 5.0

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 :

def norme(x, y, k=2):


return (x**k + y**k)**(1/k)

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

In [6]: norme(3, 4) # ici k=2


Out[6]: 5.0

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

• Portée des variables


Fréquemment, de nouvelles variables sont définies et utilisées dans le bloc d’instructions d’une fonction.
Néanmoins, celles-ci ne sont pas référencées au niveau global. En effet, chaque fonction possède sa propre table
de référencement, ce qui permet d’en limiter la portée. Autrement dit, leur contenu est inaccessible depuis
l’extérieur de la fonction et en particulier au niveau de l’interprète de commande.
De telles variables sont qualifiées de locales, par opposition aux variables globales, dont le contenu est accessible
à tout niveau.

In [9]: a = 1 # définition d'une variable globale a

In [10]: def f():


...: b = a # définition d'une variable locale b
...: return b

In [11]: f()
Out[11]: 1

In [12]: b
NameError: name 'b' is not defined

Figure 1 – Le contenu de la variable locale b n’est pas accessible en dehors de la fonction.

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 [13]: def g():


...: a = 2 # définition d'une variable locale a
...: return a

In [14]: g()
Out[14]: 2

In [15]: a # ici il s'agit de la variable globale définie ligne 9


Out[15]: 1

Figure 2 – Variables locales et globales peuvent porter le même nom.

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 [16]: def h():


...: global a # déclaration d'une variable globale
...: a = 2
...: return a

In [17]: h()
Out[17]: 2

In [18]: a # la variable globale définie ligne 9 a bien été modifiée


Out[18]: 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

Qu’affiche le shell lorsqu’on exécute le script suivant ?

a = 1
print(f(), a)
print(a, f())
print(a, g())
print(a, h())

1.2 Instructions conditionnelles


Les instructions conditionnelles se définissent à l’aide de l’instruction if et prennent la forme suivante :

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

In [2]: 'gamma' <= 'beta'


Out[2]: False

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.

• Instructions conditionnelles multiples


En informatique il est fréquent qu’on ait à imbriquer plusieurs tests, aussi existe-t-il en python un mot clé elif
(qui est la contraction de else if) et qui fonctionne suivant le schéma :
if expression booléenne 1:
bloc..............
d'instructions 1..
elif expression booléenne 2:
bloc..............
d'instructions 2..
else:
bloc..............
d'instructions 3..

– Si l’expression booléenne 1 s’évalue en True, le bloc d’instructions 1 est réalisé ;


– Si l’expression booléenne 1 s’évalue en False et l’expression booléenne 2 en True, le bloc d’instructions 2
est réalisé ;
– dans les autres cas, le bloc d’instructions 3 est réalisé.
Évidemment, plusieurs elif à la suite peuvent être utilisés pour multiplier les cas possibles, mais n’oubliez pas
que les tests sont effectués séquentiellement, c’est à dire les uns après les autres jusqu’à trouver un test positif
(ou arriver au cas final else).

Exercice 2  Rédiger une fonction tri(a, b, c) qui prend en arguments trois nombres a, b et c et qui
retourne ces trois valeurs triées par ordre croissant.

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

2.1 Boucles énumérées


• La fonction range
La fonction range peut prendre entre 1 et 3 arguments entiers :
– range(b) énumère les entiers 0, 1, 2, . . . , b − 1 ;
– range(a, b) énumère les entiers a, a + 1, a + 2, . . . , b − 1 ;
– range(a, b, c) énumère les entiers a, a + c, a + 2c . . . , a + nc où n est le plus grand entier vérifiant a + nc < b.
(On observera la similitude qui existe avec le slicing des chaînes de caractères décrit au chapitre précédent.)
Pour pouvoir illustrer le contenu de cette énumération, il faut ranger cette dernière dans une liste 5 :
In [1]: list(range(10))
Out[1]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [2]: list(range(5, 15))


Out[2]: [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

In [3]: list(range(1, 20, 3))


Out[3]: [1, 4, 7, 10, 13, 16, 19]

• 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 :

à l’entrée de la boucle indexée par k, la variable x référence la valeur de uk .

Cette assertion est vraie avant la première itération de la boucle (initialisation).


Si elle est vraie à l’entrée de la boucle indexée par k, la variable x référencera à l’entrée de la boucle suivante la
valeur 2uk + 1 = uk+1 donc cette assertion restera vraie à l’entrée de la boucle indexée par k + 1 (conservation).
Ainsi, une fois la boucle terminée la variable x référencera la valeur de un , ce qui prouve la validité de cette
fonction (terminaison).
Exemple. Considérons maintenant le calcul de n!. À l’image de l’exemple précédent, nous allons chercher à
respecter l’invariant suivant :

à l’entrée de la boucle indexée par k, la variable x référence la valeur de k!.

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 :

à l’entrée de la boucle indexée par k, la variable x référence la valeur de uk .

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 :

def mystere(p, x):


n = len(p)
s = 0
for k in range(n):
s = x * s + p[n−1−k]
return s

• Parcours d’une chaîne de caractères


Les boucles indexées que nous venons d’étudier ne sont qu’un cas particulier des possibilités offertes par la
notion d’énumération en python. Celle-ci suit la syntaxe :

for ... in ...:


bloc ...........................
................................
d'instructions .................

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.

def epeler(mot): def epeler(mot):


for c in mot: for i in range(len(mot)):
print(c, end=' ') print(mot[i], end=' ')

In [1]: epeler('Louis−Le−Grand')
L o u i s − L e − G r a n d

Figure 4 – Deux fonctions conduisant au même résultat.

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

In [2]: for (i, c) in enumerate('Louis−Le−Grand'):


...: print(i, c, sep='−>', end=' ')
0−>L 1−>o 2−>u 3−>i 4−>s 5−>− 6−>L 7−>e 8−>− 9−>G 10−>r 11−>a 12−>n 13−>d

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).

In [3]: for (i, c) in zip(range(1, 9), 'Louis−Le−Grand'):


...: print(i, c, sep='−>', end=' ')
1−>L 2−>o 3−>u 4−>i 5−>s 6−>− 7−>L 8−>e

2.2 Boucles conditionnelles


Une boucle conditionnelle exécute une suite d’instructions tant qu’une certaine condition est réalisée ; elle
peut donc tout aussi bien ne jamais réaliser cette suite d’instructions (lorsque la condition n’est pas réalisée
au départ) que de les réaliser un nombre infini de fois (lorsque la condition reste éternellement vérifiée). La
syntaxe d’une boucle conditionnelle est la suivante :

while condition:
bloc ...........................
................................
d'instructions .................

La condition doit être une expression à valeurs booléennes. Par exemple,

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

In [4]: while x > 0:


...: print(x, end=' ')
...: x −= 1
10 9 8 7 6 5 4 3 2 1

(rappelons que l’instruction x −= 1 est équivalente à x = x − 1).

• Terminaison d’une boucle conditionnelle


Le fait qu’une boucle conditionnelle puisse boucler éternellement doit nous amener à nous préoccuper de la
terminaison de celle-ci, c’est-à-dire être à même de prouver que celle-ci retourne un résultat en un temps fini. Là
encore, c’est la recherche d’un invariant de boucle adéquat qui fournit le plus souvent la solution.
Considérons la définition suivante :

Jean-Pierre Becirspahic
3.10 informatique commune

def mystere(a, b):


q, r = 0, a
while r >= b:
q, r = q + 1, r − b
return q, r

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 :

à l’entrée de la boucle indexée par n la variable q référence l’entier n et r l’entier a − nb.

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

2.3 Forcer la sortie d’une boucle


Il existe deux façons de sortir prématurément d’une boucle : en utilisant l’instruction return (dans le cadre de
la définition d’une fonction) ou l’instruction break.
Prenons l’exemple de la recherche d’un caractère particulier dans une chaîne de caractères : il faut parcourir la
chaîne caractère par caractère en comparant à chaque fois le caractère de la chaîne avec celui que l’on cherche
mais bien évidemment, une fois le caractère trouvé (s’il existe) il n’est plus nécessaire de continuer à parcourir
le reste de la chaîne. On va donc écrire :

def cherche(c, chn):


for x in chn:
if x == c:
return True
return False

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

Voici quelques expériences avec 100 000 essais :

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

On obtient (c’était prévisible) un nombre moyen de lancer égal à 36.

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.

1. Représentation dans une base


Nous sommes habitués depuis l’enfance à utiliser l’écriture en base 10 des entiers : par exemple, 2985 représente
le nombre 2 × 103 + 9 × 102 + 8 × 10 + 5 × 100 . Mais plus généralement, pour tout entier b > 2 on peut définir
la représentation en base b d’un entier en convenant que l’écriture (ap ap−1 · · · a0 )b représente le nombre :
ap × bp + ap−1 × bp−1 + · · · + a1 × b + a0 .
Pour s’assurer de l’unicité de l’écriture d’un entier dans une base donnée, il est nécessaire en outre d’imposer :
∀k ∈ ~0, p, ak ∈ ~0, p − 1 et ap , 0. Ainsi, en base 3 par exemple, seuls les chiffres 0, 1 et 2 seront utilisés, et le
nombre (210122)3 représente l’entier 2 × 35 + 34 + 32 + 2 × 3 + 2, c’est à dire 584 2 .

É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

Figure 1 – Un exemple d’addition et de multiplication en base 2


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 :

(101010)2 + (11000)2 (110101)2 − (11001)2 (11101)2 × (1011)2 (1100101)2 ÷ (1011)2

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

def base10(x, b=2):


s = 0
k = len(x) − 1
for a in x:
s += int(a) * b ** k
k −= 1
return s

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 :

def base10(x, b=2):


u = 0
for a in x:
u = b * u + int(a)
return u

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 :

def baseb(x, b=2):


s = ''
y = x
while y > 0:
s = str(y % b) + s
y //= b
return s

Par exemple :
In [3]: baseb(584, b=3)
Out[3]: '210122'

In [4]: baseb(91)
Out[4]: '1011011'

Le cas particulier de la base 16


Il a été dit plus haut que l’interaction homme/machine se fait implicitement en base 10. Il y a néanmoins deux
exceptions. Il est possible d’introduire directement au clavier un nombre écrit en base 2 ; il suffit de le faire
précéder des caractères 0b :
In [5]: 0b1011011
Out[5]: 91
Représentation des nombres 4.3

La seconde exception est la base 16, en faisant précéder le nombre des caractères 0x :
In [6]: 0xa27c
Out[6]: 41596

En effet, 10 × 163 + 2 × 162 + 7 × 16 + 12 = 41596.


La raison du rôle particulier que joue la base 16 en informatique provient du fait que l’écriture binaire d’un
nombre présente l’inconvénient d’être très longue à écrire, ce qui a incité les informaticiens à écrire ces nombres
dans une base plus élevée. Le choix de la base 10 pourrait paraître naturel, mais malheureusement convertir un
nombre de la base 10 à la base 2 ou inversement n’est pas chose facile. En revanche, nous allons voir que passer
de la base 2 à la base 16 est très simple à réaliser.

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 :

(0000)2 = (0)16 (0100)2 = (4)16 (1000)2 = (8)16 (1100)2 = (c)16


(0001)2 = (1)16 (0101)2 = (5)16 (1001)2 = (9)16 (1101)2 = (d)16
(0010)2 = (2)16 (0110)2 = (6)16 (1010)2 = (a)16 (1110)2 = (e)16
(0011)2 = (3)16 (0111)2 = (7)16 (1011)2 = (b)16 (1111)2 = (f)16

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.

Au vu de l’importance de la base 2 et de la base 16 en informatique, il existe deux fonctions qui réalisent la


conversion vers la base 2 et vers la base 16 : les fonctions bin et hex. Ces fonctions prennent en argument un
objet de type int et retournent une chaîne de caractères (précédée des caractères 0b ou 0x).
In [7]: bin(41397)
Out[7]: '0b1010000110110101'

In [8]: hex(41397)
Out[8]: '0xa1b5'

In [9]: 0b1010000110110101 + 0xa1b5


Out[9]: 82794

2. Codification des nombres entiers


Pour pouvoir être stocké et manipulé par un ordinateur, un nombre doit être représenté par une succession de
bits. Le principal problème est la limitation de la taille du codage : un nombre mathématique peut prendre des
valeurs arbitrairement grandes, tandis que le codage dans l’ordinateur doit s’effectuer sur un nombre de bits
fixé.

2.1 Les entiers naturels


Les entiers naturels sont essentiellement utilisés pour représenter les adresses en mémoire. Un codage sur n
bits permet de représenter tous les nombres naturels compris entre 0 et 2n − 1. Ainsi, un octet permet de coder
les entiers allant de 0 = (00)16 = (0000 0000)2 à 255 = (ff)16 = (1111 1111)2 , et 64 bits (soit 8 octets) tous les
nombres allant de 0 = (0000 0000 0000 0000)16 à 264 − 1 = (ffff ffff ffff ffff)16 .
3. les composantes Rouge Vert Bleu en synthèse additive.

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

– l’entier naturel 2p − 1 est représenté par 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1

p bits

2.2 Les entiers relatifs


Pour représenter un entier relatif il est nécessaire de coder le signe de ce dernier sur un bit (0 pour les nombres
positifs et 1 pour les nombres négatifs) ; ainsi le processeur va réserver le premier bit pour le signe, et les n − 1
autres bits pour l’entier à proprement parler. Le plus grand entier relatif positif représentable sur n bits est
donc égal à 2n−1 − 1. En base 2, il s’écrit : (0 111 1111 · · · 1111)2 .
| {z }
n − 1 chiffres 1

représentation binaire de x

Figure 2 – Représentation machine d’un entier relatif positif.

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

Le codage des nombres négatifs


On pourrait s’attendre à ce que la représentation d’un entier relatif négatif n soit un 1 (le bit de signe) suivi de
la représentation binaire de |n|. Il n’en est rien, car cette méthode possède plusieurs inconvénients :
– Le nombre 0 posséderait deux représentations (et il est toujours préférable d’avoir unicité de la représen-
tation d’un objet) ;
– L’algorithme d’addition ne s’applique qu’à des entiers de même signe et cette représentation le rendrait
inapplicable pour additionner des entiers relatifs.
C’est pourquoi on utilise le codage particulier pour représenter les entiers négatifs, dit en complément à deux :
le nombre négatif x est représenté en mémoire par la représentation binaire de l’entier (positif) 2n + x.

représentation binaire de 2n + x

Figure 3 – Représentation machine d’un entier relatif négatif.

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.

Addition de deux entiers relatifs


La première conséquence de la représentation par complément à deux est que tout entier de l’intervalle
~−2n−1 , 2n−1 − 1 possède une unique représentation. Mais l’intérêt majeur n’est pas là : nous allons le constater
qu’additionner deux nombres relatifs revient à additionner leurs représentations en ne gardant que les n bits de
poids faible.
Avant de le justifier, illustrons ce résultat en calculant la somme de −91 et de 113 avec un codage sur 8 bits (on
a 28 = 256 ; les nombres représentables sont compris entre −128 et 127).
−91 est négatif donc il est représenté par son complément à deux 256 − 91 = 165 = (10100101)2 .
113 est positif donc il est représenté par sa décomposition en base 2 : 113 = (01110001)2 .
On additionne ces deux représentations :

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.

Soustraction de deux entiers relatifs


Calculer la soustraction a − b ça n’est jamais qu’additionner a et −b ; s’il est possible de calculer facilement
l’opposé avec la représentation en complément à deux nous pourrons ramener toute soustraction à une addition.
Considérons donc un entier x et considérons une fois de plus deux cas :

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 :

262 − 1 = 4611686018427387903 et − 262 = −4611686018427387904.

Entiers de taille arbitraire


Quant est-il de python ? Pour y répondre, rien de plus simple, il suffit de regarder quel est le successeur du plus
grand entier représentable en complément à deux sur 64 bits :
In [2]: x = 0x7fffffffffffffff

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.

3. Codification des nombres décimaux


3.1 Les nombres flottants
x
Rappelons qu’un nombre décimal est un rationnel qui peut s’écrire sous la forme où x est un entier relatif.
10n
Si on considère la représentation en base 10 de l’entier x = ±(ap ap−1 · · · a1 a0 )10 , on obtient l’écriture décimale de
x x
n
en plaçant une virgule séparant les n chiffres les plus à droite des autres : n = ±(ap · · · an , an−1 · · · a0 )10 .
10 10
25 625
Par exemple, est un nombre décimal car il peut aussi s’écrire , et son écriture décimale est 6, 25.
4 100
x
De la même façon, un nombre dyadique est un rationnel qui peut s’écrire sous la forme n où x est un entier
2
relatif, et son développement dyadique s’obtient en plaçant une virgule séparant les n chiffres les plus à droite
des autres.
25
Par exemple, est un nombre dyadique et son développement dyadique est (110, 01)2 . En effet,
4
25 1 1
25 = (11001)2 et (110, 01)2 = 1 × 22 + 1 × 21 + 0 × 20 + 0 × 1 + 1 × 2 = 6, 25.
4 2 2
On s’en doute, contrairement aux humains les ordinateurs ne vont pas manipuler des nombres décimaux mais
des nombres dyadiques. Ce n’est pas un problème quand il s’agit d’entiers puisque dans ce cas la conversion

4. C’est le cas en général des langages compilés.

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

In [2]: (0.1 + 0.2) − 0.3


Out[2]: 5.551115123125783e−17

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.

In [3]: 0.1 + 0.2 == 0.3


Out[3]: False

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 :

0 6 e0 6 211 − 1 ⇐⇒ 1 − 210 6 e 6 210 soit − 1023 6 e 6 1024.

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

– si (00000000001)2 6 e0 6 (11111111110)2 , autrement dit si 1 6 e0 6 211 − 2, le nombre est représenté sous sa


forme normalisée standard. Nous avons alors −1022 6 e 6 1023 ; ainsi le plus petit nombre strictement positif
représentable sous forme normalisée est :

(1, 0000 · · · 0000)2 × 2−1022 = 2−1022 ≈ 2, 23 × 10−308


| {z }
52 fois 0

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).

In [1]: 2e308 # dépasse le plus grand nombre représentable


Out[1]: inf

In [2]: sqrt(−1) # calcul invalide


Out[2]: nan

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

In [7]: a < 100000000000000000000000000000000000000


Out[7]: False


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

3.2 Calculs sur les nombres flottants


La représentation interne des nombres flottants a une incidence sur les opérations que l’on est susceptible de
demander à l’ordinateur, et provoque quelques désagréments dont il faut avoir conscience. Sans prétendre à
l’exhaustivité, nous allons passer en revue quelques-unes des conséquences de cette représentation.

Erreurs d’arrondis dues aux changement de bases


Nous l’avons déjà constaté, la conversion d’un nombre décimal en nombre dyadique et réciproquement provoque
des erreurs d’arrondis qui ne sont pas sans conséquences :
In [5]: (0.1 + 0.2) − 0.3
Out[5]: 5.551115123125783e−17

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 :

1, 1001 1001 · · · 1001 1010 × 2−4


| {z }
48 bits

Sans surprise, la représentation machine de 0, 2 est le double de celle-ci, à savoir :

1, 1001 1001 · · · 1001 1010 × 2−3


| {z }
48 bits

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 :

10, 0110 0110 · · · 0110 0111 × 2−3


| {z }
48 bits

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

C’est bien cela !

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. Structures de données linéaires


Dans son acceptation la plus générale, une structure de données spécifie la façon de représenter en mémoire
machine les données d’un problème à résoudre en décrivant :
– la manière d’attribuer une certaine quantité de mémoire à cette structure ;
– la façon d’accéder aux données qu’elle contient.
Dans certains cas, la quantité de mémoire allouée à la structure de donnée est fixée au moment de la création
de celle-ci et ne peut plus être modifiée ensuite ; on parle alors de structure de données statique. Dans d’autres
cas l’attribution de la mémoire nécessaire est effectuée pendant le déroulement de l’algorithme et peut donc
varier au cours de celui-ci ; il s’agit alors de structure de données dynamique. Enfin, lorsque le contenu d’une
structure de donnée est modifiable, on parle de structure de donnée mutable.
Les structures de données classiques appartiennent le plus souvent aux familles suivantes :
– les structures linéaires : il s’agit essentiellement des structures représentables par des suites finies ordon-
nées ; on y trouve les tableaux et les listes chaînées, qui vont nous intéresser par la suite ;
– les matrices ou tableaux multidimensionnels ;
– les structures arborescentes (en particulier les arbres binaires) ;
– les structures relationnelles (bases de données ou graphes pour les relations binaires).
Nous nous intéresserons avant tout aux deux premières de ces familles.

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

Figure 1 – Une représentation d’un tableau en mémoire.

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

1.2 Listes chaînées


Les listes chaînées associent à chaque donnée (de même type) un pointeur indiquant la localisation dans la
mémoire de la donnée suivante (à l’exception de la dernière, qui pointe vers une valeur particulière indiquant
la fin de la liste).

a nil

Figure 2 – Une représentation d’une liste en mémoire.

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.

1.3 La classe list en Python


Contrairement à ce que pourrait laisser croire son nom, la classe list n’est pas une liste chaînée au sens qu’on
vient de lui donner, mais une structure de donnée plus complexe qui cherche à concilier les avantages des
tableaux et des listes chaînées, à savoir :
être une structure de donnée dynamique dans laquelle les éléments sont accessibles à coût constant.
Dans la description de cette classe qui va suivre, on pourra distinguer les opérations qui relèvent de la structure
de tableau (accès direct aux éléments) de ceux qui relèvent de la structure de liste chaînée (ajout/suppression
d’éléments).

2. Construction, modification et accès à une liste


2.1 Définition et accès aux éléments
Une liste est une structure de données linéaire, les objets étant enclos par des crochets et séparés par des
virgules. Par exemple,
a = [0, 1, 'abc', 4.5, 'de', 6]

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]

2.3 Création d’une liste par compréhension


Les listes sont de type list et à l’instar des autres types déjà rencontrés, il existe une fonction list qui convertit
lorsque c’est possible un objet d’un certain type vers le type list. C’est le cas en particulier des énumérations
produites par la fonction range 3 . Par exemple :

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]

In [15]: list(range(13, 2, −3))


Out[15]: [13, 10, 7, 4]

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 :

In [17]: [x for x in range(11) if x * x <= 50]


Out[17]: [0, 1, 2, 3, 4, 5, 6, 7]

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]

In [19]: [(x, y) for x in a for y in b]


Out[19]: [(1, 2), (1, 4), (1, 6), (3, 2), (3, 4), (3, 6), (5, 2), (5, 4), (5, 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 !

2.4 Opérations sur les listes


À l’instar des chaînes de caractères, deux opérations sont possibles sur les listes : la concaténation et la
duplication.
La concaténation de deux listes consiste simplement à les mettre bout à bout dans une nouvelle liste. L’opération
de concaténation se note + :
In [1]: [2, 4, 6, 8] + [1, 3, 5, 7]
Out[1]: [2, 4, 6, 8, 1, 3, 5, 7]

(attention, ce n’est pas un opérateur commutatif).


L’opérateur de duplication se note * ; si l est une liste et n un entier alors l * n est équivalent à l + l +...+ l
(n fois).
In [2]: [1, 2, 3] * 3
Out[2]: [1, 2, 3, 1, 2, 3, 1, 2, 3]
Listes et séquences 5.5

2.5 Mutation d’une liste


En python, une liste est un objet mutable, dans le sens où il est possible de modifier le contenu d’une liste. Il est
possible de modifier un ou plusieurs éléments de la liste, d’en supprimer ou d’en ajouter.

Modification d’un élément


Si l est une liste, l’instruction l[i] = x remplace l’élément d’indice i par la valeur x.
In [1]: l = list(range(11))

In [2]: l[3] = 'a'

In [3]: l
Out[3]: [0, 1, 2, 'a', 4, 5, 6, 7, 8, 9, 10]

Modification de plusieurs éléments


Le slicing permet de remplacer une partie de la liste : l’instruction l[i:j] = t remplace les éléments d’indices
i, i + 1, . . . , j − 1 par les éléments de la séquence 4 t.
In [4]: l[3:7] = 'abc'

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 [7]: l[1::2] = l[0::2]

In [8]: l
Out[8]: [0, 0, 2, 2, 4, 4, 6, 6, 8, 8]

Suppression d’un ou plusieurs éléments


L’instruction del permet de supprimer un ou plusieurs éléments (définis par le slicing) :
In [9]: l = list(range(11))

In [10]: del l[3:6]

In [11]: l
Out[11]: [0, 1, 2, 6, 7, 8, 9, 10]

Insertion dans une liste


L’insertion d’un élément dans une liste fonctionne suivant une technique différente : lorsqu’on crée une liste, on
crée un objet à qui est associé une structure de données et un certain nombre de méthodes, qui sont en quelque
sorte des fonctions particulières associées à ce type d’objet 5 . Lorsque obj est un représentant de cette catégorie
d’objet et meth une méthode associée, on applique cette dernière à l’objet en suivant la syntaxe : obj.meth()
Il existe principalement deux méthodes associées aux listes qui permettent l’insertion de nouveaux éléments : la
méthode append(x) ajoute l’élément x en fin de liste, et la méthode insert(i, x) insère l’élément x à l’index
i.

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 [12]: l = ['a', 'b', 'c', 'd']

In [13]: l.append('e')

In [14]: l
Out[14]: ['a', 'b', 'c', 'd', 'e']

In [15]: l.insert(2, 'x')

In [16]: l
Out[16]: ['a', 'b', 'x', 'c', 'd', 'e']

Autres méthodes associées aux listes


Il existe d’autres méthodes associées aux listes ; on retiendra en particulier :
– la méthode remove(x) supprime la première occurence de x dans la liste ;
– la méthode pop(i) supprime l’élément d’indice i et retourne cet élément ;
– la méthode reverse() inverse l’ordre des éléments d’une liste ;
– la méthode sort() trie la liste (par ordre croissant).

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

3. Un mot sur les séquences


Vous avez dû observer des similitudes entre les chaînes de caractères et les listes : elles sont composées d’un
nombre fini d’éléments auxquels on peut accéder par un indice, et elles se prêtent aux mêmes techniques de
slicing. De tels objets sont appelés des séquences ; il en existe d’autres, par exemple les tuples, qui servent à
regrouper des éléments par couples, triplets, quadruplets, (et plus généralement les n-uplets).
Toutes ces séquences ont des propriétés en commun. Si seq est une séquence, alors :
– seq[k] désigne l’élément d’indice k de cette séquence (la numérotation commençant à 0) ;
– on peut effectuer des coupes à l’aide de la technique du slicing ;
– la fonction len donne la longueur de la séquence ;
– la concaténation et la duplication se notent respectivement + et * ;
Nous utiliserons principalement :
– les chaînes de caractères, qui sont des successions de caractères délimités par des guillemets (simples ou
doubles) ;
– les listes, qui sont des successions d’objets séparés par des virgules et délimités par des crochets ;
– les tuples, qui sont des successions d’objets séparés par des virgules et délimités par des parenthèses.
Les fonctions str, list, tuple permettent de convertir un type de séquence en un autre ; par exemple :

In [1]: list('abcde')
Out[1]: ['a', 'b', 'c', 'd', 'e']

In [2]: tuple([1, 2, 3, 4, 5])


Out[2]: (1, 2, 3, 4, 5)

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)

In [9]: tup1 = (4, 2, 3)


(4, 2, 3)
In [10]: id(tup1)
Out[10]: 4448266640

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.

Les pièges de la mutabilité


Une méconnaissance du mécanisme de la mutabilité peut avoir une conséquence facheuse lorsqu’on cherche
à créer une copie d’un objet mutable. En effet, lorsque var1 est une variable, l’instruction var2 = var1 se
contente de créer un nouveau référencement vers le même emplacement en mémoire. Par exemple, les instructions
l2 = l1 et tup2 = tup1 créent la situation suivante :

In [11]: l2 = l1

In [12]: id(l1) == id(l2)


Out[12]: True l1 [4, 2, 3] tup1

In [13]: tup2 = tup1


l2 tup2 (4, 2, 3)
In [14]: id(tup1) == id(tup2)
Out[14]: True

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)

In [17]: tup1 = (1, 2, 3)


l2 tup2 (4, 2, 3)
In [18]: tup2
Out[18]: (4, 2, 3)

Copie d’un objet mutable


Comment alors copier un élément mutable ? Nous venons de constater qu’un simple référencement n’est pas
suffisant. Il est nécessaire de recréer une liste qu’on veut copier. Cette copie peut se faire par exemple à l’aide du
slicing [:], puisque ceci recrée une nouvelle liste.
l1 [1, 2, 3]
In [19]: l3 = l1[:]

In [20]: id(l1) == id(l3) l2


Out[20]: False
l3 [1, 2, 3]

6. C’est effectivement le cas.


Listes et séquences 5.9

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.

In [21]: l1 = [[1, 2, 3], [4, 5, 6]]


l1[0]
In [22]: l2 = l1[:] l1 [1, 2, 3]
l1[1]
In [23]: id(l1) == id(l2)
Out[23]: False
l2[0]
l2 [4, 5, 6]
In [24]: id(l1[0]) == id(l2[0]) l2[1]
Out[24]: True

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 :

In [25]: from copy import deepcopy


l1[0] [1, 2, 3]
In [26]: l3 = deepcopy(l1) l1
l1[1] [4, 5, 6]
In [27]: id(l1) == id(l3)
Out[27]: False
l3[0] [1, 2, 3]
l3
In [28]: id(l1[0]) == id(l3[0]) l3[1] [4, 5, 6]
Out[28]: False

4. Parcours d’une liste


Nous allons maintenant nous intéresser à la façon de parcourir une liste. Puisqu’il ne s’agit pas ici de modifier
les éléments de la liste mais seulement d’y accéder, ce qu’on va dire pourra aussi s’appliquer aux chaînes de
caractères et aux tuples.

4.1 Parcours complet par boucle énumérée


Les boucles énumérées for i in range(...) que nous avons utilisées jusqu’à présent ne sont qu’un cas
particulier d’une syntaxe plus générale :

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

Quelques exemples d’application :

• 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

qui permet le calcul de la variance après un seul parcours de liste :

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

4.2 Parcours incomplet : recherche dans une liste


Les algorithmes de recherche dans une liste correspondant le plus souvent à des parcours incomplets, puisqu’on
stoppe la recherche une fois l’élément trouvé. On utilise en général une boucle conditionnelle, à moins qu’on
préfère utiliser une sortie prématurée de boucle énumérée.

• Recherche d’un élément


Un problème fréquent est de déterminer la présence ou non d’un élément x dans une liste l. On peut répondre
à ce problème de plusieurs façons.
La première version, la plus classique, consiste à parcourir la liste à l’aide d’une boucle conditionnelle : on
continue le parcours tant que l’élément n’est pas trouvé et qu’on est pas en bout de liste.

def cherche(x, l):


k, rep = 0, False
while k < len(l) and not rep:
rep = l[k] == x
k += 1
return rep

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é :

def cherche(x, l):


for y in l:
if y == x:
return True
return False

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

qui donne un booléen correspondant à la réponse cherchée.

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.

def cherche_dicho(x, l):


i, j = 0, len(l)
while i < j:
k = (i + j) // 2
if l[k] == x:
return True
elif l[k] > x:
j = k
else:
i = k + 1
return False

Évaluation du nombre de comparaison


Notons cn le nombre de comparaison que cet algorithme effectue dans le pire des cas. Le principe dichotomique
jn − 1k jnk
sépare la liste en trois parties de respectivement , 1 et cases, et une comparaison permet de restreindre
2 2
la recherche à l’une de ces trois sous-listes.
Dans le pire des cas, on a donc : cn = 1 + cbn/2c .
On résout cette relation de récurrence en utilisant la décomposition en base 2 de n : n = (bp · · · b1 b0 )2 car dans ce
jnk
cas, = (bp · · · b2 b1 )2 . Ainsi, cn = p + c1 = p + 1.
2
Il reste à exprimer l’entier p en fonction de n. Or : (1 00 · · · 00)2 6 n 6 (1 11 · · · 11)2 donc 2p 6 n 6 2p+1 − 1.
| {z } | {z }
p fois p fois
En composant par le logarithme en base 2 on obtient : p 6 log2 (n) < p + 1, et donc p = blog2 (n)c.
Le nombre de comparaisons dans le pire des cas de l’algorithme de recherche dichotomique est donc égal
à blog2 (n)c + 1, à comparer aux n comparaisons que nécessite l’algorithme de recherche linéaire, ce qui peut
constituer une différence importante dans le cas de nombreuses recherches dans des listes de grande taille.
informatique commune
Chapitre 6
Notion de complexité
algorithmique

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.

def diviseurs1(n): def diviseurs2(n):


d = 0 d = 0
k = 1 k = 1
while k <= n: while k * k < n:
if n % k == 0: if n % k == 0:
d += 1 d += 2
k += 1 k += 1
return d if k * k == n:
d += 1
return d

Figure 1 – Deux fonctions calculant le nombre de diviseurs d’un entier n.


n √
Mais on peut aussi se faire la réflexion suivante : si d ∈ ~1, n divise n alors d 0 = aussi ; de plus, d 6 n ⇐⇒
√ d √
d 0 > n. Autrement dit, il suffit de rechercher les diviseurs de n qui sont inférieurs ou égaux à n pour
en connaitre le nombre total : c’est le principe utilisé par la fonction diviseurs2 (on notera le traitement
particulier réservé aux carrés parfaits).
Comment comparer ces deux versions ? Si on se focalise sur les deux boucles conditionnelles de ces algorithmes
on constate que dans les deux cas on effectue deux additions,
√ une division euclidienne et un test. Chacune de
ces opérations est effectuée n fois dans le premier cas, n fois 2 dans le second. Nous ne connaissons pas le
temps τ1 nécessaire à la réalisation de ces différents calculs, mais on peut légitimement penser que le temps
total d’execution de diviseurs1 n’est pas éloigné de τ1 n + τ2 , le temps τ2 étant le temps
√ requis par les autres
opérations. De même, le temps requis par la fonction diviseurs2 est de l’ordre de τ10 n + τ20 .
Les temps τ2 et τ20 sont négligeable pour de grandes valeurs de n ; de plus les valeurs de τ1 et τ10 importent peu ;
elle dépendent de conditions matérielles qui nous échappent. Nous n’allons retenir que le taux de croissance de
chacune de ces deux fonctions : proportionnel
√ à n pour la première (on dira plus loin qu’il s’agit d’un algorithme
de coût linéaire), proportionnel à n pour la seconde. Autrement dit :
– multiplier n par 100 multiplie le temps d’exécution de diviseurs1 par 100 ;
– multiplier n par 100 multiplie le temps d’exécution de diviseurs2 par 10.
Ainsi, connaissant le temps d’exécution pour une valeur donnée de n il est possible d’évaluer l’ordre de grandeur
du temps d’exécution pour de plus grandes valeurs.

1. On dit aussi le coût.



2. tres exactement d ne − 1 fois.

Jean-Pierre Becirspahic
6.2 informatique commune

2. Évaluation de la complexité algorithmique


2.1 Instructions élémentaires
Pour réaliser l’évaluation de la complexité algorithmique, il est nécessaire de préciser un modèle de la tech-
nologie employée ; en ce qui nous concerne, il s’agira d’une machine à processeur unique pour laquelle les
instructions seront exécutées l’une après l’autre, sans opération simultanées. Il faudra aussi préciser les instruc-
tions élémentaires disponibles ainsi que leurs coûts. Ceci est particulièrement important lorsqu’on utilise un
langage de programmation tel que Python pour illustrer un cours d’algorithmique car ce langage possède de
nombreuses instructions de haut niveau qu’il serait irréaliste de considérer comme ayant un coût constant : il
existe par exemple une fonction sort qui permet de trier un tableau en une instruction, mais il serait illusoire
de croire que son temps d’exécution est indépendant de la taille du tableau.
La première étape consiste donc à préciser quelles sont les instructions élémentaires, c’est-à-dire celle qui
seront considérées comme ayant un coût constant, indépendant de leurs paramètres. Parmi celles-ci figurent en
général :
– les opérations arithmétiques (addition, soustraction, multiplication, division, modulo, partie entière, . . .)
– la comparaisons de données (relation d’égalité, d’infériorité, . . .)
– le transferts de données (lecture et écriture dans un emplacement mémoire)
– les instructions de contrôle (branchement conditionnel et inconditionnel, appel à une fonction auxiliaire,
. . .)
Attention : il est parfois nécessaire de préciser la portée de certaines de ces instructions. En arithmétique
par exemple, il est impératif que les données représentant les nombres soient codées sur un nombre fixe de
bits. C’est le cas en général des nombres flottants (le type float) et des entiers relatifs (le type int) représentés
usuellement sur 64 bits 3 , mais dans certains langages existe aussi un type entier long dans lequel les entiers
ne sont pas limités en taille. C’est le cas en Python, où coexistaient jusqu’à la version 3.0 du langage une
classe int et une classe long. Ces deux classes ont depuis fusionné, le passage du type int au type long étant
désormais transparent pour l’utilisateur. Cependant, dans un but de simplification nous considérerons désormais
toute opération arithmétique comme étant de coût constant.
Dans le cas des nombres entiers, l’exponentiation peut aussi être source de discussion : s’agit-t’il d’une opé-
ration de coût constant ? En général on répond à cette question par la négative : le calcul de nk nécessite un
nombre d’opérations élémentaires (essentiellement des multiplications) qui dépend de k. Cependant, certains
processeurs possèdent une instruction permettant de décaler de k bits vers la gauche la représentation binaire
d’un entier, autrement dit de calculer 2k en coût constant.
Les comparaisons entre nombres (du moment que ceux-ci sont codés sur un nombre fixe de bits) seront aussi
considérées comme des opérations à coût constant, de même que la comparaison entre deux caractères. En
revanche, la comparaison entre deux chaînes de caractères ne pourra être considérée comme une opération
élémentaire, même s’il est possible de la réaliser en une seule instruction Python. Il en sera de même des
opérations d’affectation : lire ou modifier le contenu d’un case d’un tableau est une opération élémentaire, mais
ce n’est plus le cas s’il s’agit de recopier tout ou partie d’un tableau dans un autre, même si la technique du
slicing en Python permet de réaliser très simplement ce type d’opération.

2.2 Notations mathématiques


Une fois précisé la notion d’opération élémentaire, il convient de définir ce qu’on appelle la taille de l’entrée.
Cette notion dépend du problème étudié : pour de nombreux problèmes, il peut s’agir du nombre d’éléments
constituant les paramètres de l’algorithme (par exemple le nombre d’éléments du tableau dans le cas d’un
algorithme de tri) ; dans le cas d’algorithmes de nature arithmétique (le calcul de nk par exemple) il peut s’agir
d’un entier passé en paramètre, voire du nombre de bits nécessaire à la représentation de ce dernier. Enfin, il
peut être approprié de décrire la taille de l’entrée à l’aide de deux entiers (le nombre de sommets et le nombre
d’arêtes dans le cas d’un algorithme portant sur les graphes).
Une fois la taille n de l’entrée définie, il reste à évaluer en fonction de celle-ci le nombre f (n) d’opérations
élémentaires requises par l’algorithme. Mais même s’il est parfois possible d’en déterminer le nombre exact, on
se contentera le plus souvent d’en donner l’ordre de grandeur à l’aide des notations de Landau.

3. Voir le chapitre 4.
Notion de complexité algorithmique 6.3

La notation la plus fréquemment utilisée est le « grand O » :

f (n) = O(αn ) ⇐⇒ ∃B > 0 f (n) 6 Bαn .

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 :

f (n) = Ω(αn ) ⇐⇒ ∃A > 0 Aαn 6 f (n).

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 .

Ordre de grandeur et temps d’exécution


Nous l’avons dit, la détermination de la complexité algorithmique ne permet pas d’en déduire le temps
d’exécution mais seulement de comparer entre eux deux algorithmes résolvant le même problème. Cependant, il
importe de prendre conscience des différences d’échelle considérables qui existent entre les ordres de grandeurs
usuels que l’on rencontre. En s’appuyant sur une base de 109 opérations par seconde, le tableau de la figure 2 est
à cet égard significatif. Il indique en fonction de la taille n de l’entrée (102 , 103 , . . .) et du nombre d’opérations
requis par un algorithme (log n, n, . . . ) le temps d’exécution de ce dernier.

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

Figure 2 – Temps nécessaire à l’exécution d’un algorithme en fonction de sa complexité.

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

Figure 3 – Qualifications usuelles des complexités.

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 :

def f1(n): def f2(n):


x = 0 x = 0
for i in range(n): for i in range(n):
for j in range(n): for j in range(i):
x += 1 x += 1
return x return x

def f3(n): def f4(n):


x = 0 x, i = 0, n
for i in range(n): while i > 1:
j = 0 x += 1
while j * j < i: i //= 2
x += 1 return x
j += 1
return x

def f5(n): def f6(n):


x, i = 0, n x, i = 0, n
while i > 1: while i > 1:
for j in range(n): for j in range(i):
x += 1 x += 1
i //= 2 i //= 2
return x return x

2.3 Différents types de complexité


Certains algorithmes ont un temps d’exécution qui dépend non seulement de la taille des données mais de ces
données elles-mêmes. Dans ce cas on distingue plusieurs types de complexités :
– la complexité dans le pire des cas : c’est un majorant du temps d’exécution possible pour toutes les entrées
possibles d’une même taille. On l’exprime en général à l’aide de la notation O.
– la complexité dans le meilleur des cas : c’est un minorant du temps d’exécution possible pour toutes les
entrées possibles d’une même taille. On l’exprime en général à l’aide de la notation Ω. Cependant cette
notion n’est que rarement utilisée car souvent peu pertinente au regard des complexités dans le pire des
cas et en moyenne.
– la complexité en moyenne : c’est une évaluation du temps d’exécution moyen portant sur toutes les entrées
possible d’une même taille supposées équiprobables.
La plupart du temps on se contentera d’analyser la complexité dans le pire des cas.
Exemple. Considérons les algorithmes de recherche dans une liste de longueur n ; nous en avons écrit deux
dans le chapitre précédent (voir figure 4).
– L’algorithme de recherche séquentielle effectue dans le meilleur des cas une seule comparaison (lorsque le
premier élément testé est l’élément recherché) et dans le pire des cas n comparaisons (par exemple lorsque
l’élément recherché ne se trouve pas dans la liste). On dira que le coût dans le meilleur des cas est constant, ce
qu’on traduira par un coût en Θ(1), et linéaire dans le pire des cas, c’est-à-dire une complexité en Θ(n).
Dans tous les cas la complexité est donc un O(n).
– Lorsque la liste est triée, l’algorithme de recherche dichotomique effectue lui aussi une seule comparaison
dans le meilleur des cas (lorsque l’élément recherché se trouve au milieu de la liste) et dans le pire des cas un
nombre de comparaison proportionnel à log n. En effet, si C(n) désigne la complexité dans le pire des cas on
dispose de la relation : C(n) = C(n/2) + Θ(1). Ce type de relation est souvent étudié dans le cas particulier des
puissances de 2 en posant up = C(2p ) car alors on dispose de la relation up = up−1 + Θ(1) qui conduit à up = Θ(p),
soit C(n) = Θ(log n) lorsque n = 2p .
Dans tous les cas, la complexité est donc un O(log n).
Notion de complexité algorithmique 6.5

def cherche(x, l): def cherche_dicho(x, l):


for y in l: i, j = 0, len(l)
if y == x: while i < j:
return True k = (i + j) // 2
return False if l[k] == x:
return True
elif l[k] > x:
j = k
else:
i = k + 1
return False

Figure 4 – Les algorithmes de recherche linéaire et de recherche dichotomique.

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.

2.4 Complexité spatiale


De la même façon qu’on définit la complexité temporelle d’un algorithme pour évaluer ses performances en
temps de calcul, on peut définir sa complexité spatiale pour évaluer sa consommation en espace mémoire. Le
principe est le même sauf qu’ici on cherche à évaluer l’ordre de grandeur du volume en mémoire utilisé : il
ne s’agit pas d’évaluer précisément combien d’octets sont consommés par un algorithme mais de préciser son
taux de croissance en fonction de la taille n de l’entrée. Cependant, on notera que la complexité spatiale est
bien moins que la complexité temporelle un frein à l’utilisation d’un algorithme : on dispose aujourd’hui le
plus souvent d’une quantité pléthorique de mémoire vive, ce qui rend moins important la détermination de la
complexité spatiale.

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. Égalité entre nombres flottants


De ceci il résulte que parler d’égalité entre nombres flottants n’a en général pas de sens car les nombres que l’on
cherche à comparer sont souvent le résultat de plusieurs calculs, chacun d’eux étant potentiellement entachés
de plusieurs approximations, sans même parler de l’approximation initiale issue de la conversion approximative
entre nombres décimaux et nombres dyadiques.
Dès lors que les quantités manipulées sont des flottants, il est donc potentiellement très risqué d’assurer la
terminaison d’un algorithme par un test d’égalité x == y. On préférera en général une condition traduisant
peu ou prou le fait que les quantités x et y sont « proches », dans un sens qu’il conviendra de préciser.

1.1 Comment définir la « proximité » de deux nombres ?


Une première solution, simple, consiste à choisir une marge d’erreur absolue : étant donnée une valeur ε > 0
fixée arbitrairement (par exemple ε = 10−12 ), sont considérés comme proches deux nombres x et y tels que
|x − y| 6 ε.
Par souci de simplicité il pourra arriver qu’on fasse un tel choix, mais ce n’est en général pas une bonne solution,
car une valeur choisie dans l’absolu parce qu’elle nous parait petite peut se révéler trop grande lorsque les
nombres à comparer sont eux-même très petits (on conçoit bien que comparer la proximité de deux nombres de
l’ordre de 10−18 avec ε = 10−12 n’a pas grand sens) ou au contraire très grands.
Une solution plus raisonnable consiste à mesurer l’erreur relative : sont considérés comme égaux deux nombres
flottants qui vérifient |x −y| 6 ε|y|, où ε est une valeur « petite » fixée arbitrairement. Mais cette situation présente
aussi des inconvénients :
– cette relation n’est pas symétrique : on peut avoir |x − y| 6 ε|y| et |y − x| > ε|x| ;
– cette relation présente un défaut majeur lorsque x et y sont de signes opposés : si y = −x cette relation
ne sera jamais vérifiée dès lors que ε < 2, même lorsque x et y seront égaux aux plus petits des nombres
flottants non nuls.
Bref, le problème de l’égalité de deux nombres flottants ne possède pas de réponse simple et absolue, et demande
souvent un traitement au cas par cas. Une telle discussion est esquissée dans l’exemple qui suit, mais dans la
suite de ce cours et pour des raisons de simplicité on se contentera le plus souvent d’une marge d’erreur relative
voire absolue (mais en gardant à l’esprit le problème que cela pose).
Remarque. Il existe dans le module Numpy une fonction isclose dédiée à la comparaison de deux nombres
flottants. Cette fonction possède deux valeurs rtol (par défaut égale à εr = 10−5 ) et atol (par défaut égale à
εa = 10−8 ) et renvoie la valeur True dès lors que |x − y| 6 εa + εr |y|. On observera que cette fonction combine
une tolérance absolue et une tolérance relative ; néanmoins elle n’est pas symétrique et dans de rares cas
isclose(x, y) peut retourner True et isclose(y, x) retourner False. Depuis la version 3.5 de Python il
existe dans le module math une fonction de même nom qui elle utilise la relation |x − y| 6 εa + εr max(|x|, |y|)
(avec des valeurs par défaut εr = 10−9 et εa = 0).

1. Se référer au chapitre 4.

Jean-Pierre Becirspahic
7.2 informatique commune

1.2 Résolution d’une équation du second degré


Le problème de l’égalité à 0 peut aisément être mis en évidence en cherchant à rédiger une fonction résolvant
une équation du second degré à coefficients réels de la forme : ax2 + bx + c = 0. La figure 1 présente une solution
naïve à ce problème.

from numpy import sqrt

def solve(a, b, c):


delta = b * b − 4 * a * c
if delta < 0:
print("pas de solution")
elif delta > 0:
x, y = (−b−sqrt(delta))/2/a, (−b+sqrt(delta))/2/a
print("deux racines simples {} et {}".format(x, y))
else:
x = −b/2/a
print("une racine double {}".format(x))

Figure 1 – Résolution naïve d’une équation du second degré.

Testons-la avec les valeurs a = 0, 01, b = 0, 2 et c = 1 puis avec a = 0, 011025, b = 0, 21, et c = 1 :


>>> solve(0.01, 0.2, 1)
deux racines simples −10.000000131708903 et −9.999999868291098
>>> solve(0.011025, 0.21, 1)
pas de solution

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.

def solve2(a, b, c, epsilon=2**(−52)):


delta = b * b − 4 * a * c
if delta < −epsilon*b**2:
print("pas de solution")
elif delta > epsilon*b**2:
x, y = (−b−sqrt(delta))/2/a, (−b+sqrt(delta))/2/a
print("deux racines simples {} et {}".format(x, y))
else:
x = −b/2/a
print("une racine double {}".format(x))

Figure 2 – Résolution numérique d’une équation du second degré.

Testons-la avec le même jeu de valeurs :


>>> solve2(0.01, 0.2, 1)
une racine double −10.0
>>> solve2(0.011025, 0.21, 1)
une racine double −9.523809523809524

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

représentation machine de 1 + ε est :

1 + ε = (1, 0000 · · · 0000 1)2 × 20


| {z }
51 fois 0

(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.

2. Recherche d’une racine par dichotomie


Le premier problème numérique auquel nous allons nous intéresser est celui de la recherche d’une valeur
approchée de la racine d’une fonction. D’un point de vue mathématique, nous considérons une fonction continue
f : [a, b] → R telle que f (a) et f (b) soient de signes opposés, ce qui se traduit par la relation : f (a)f (b) 6 0. Le
théorème des valeurs intermédiaires assure alors qu’il existe un réel c ∈ [a, b] tel que f (c) = 0 (illustration figure
3).

c b x
a
a+b
2

Figure 3 – Recherche d’une racine par dichotomie.

2.1 Description de la méthode


a + b
Le principe de la recherche dichotomique consiste à calculer f . Suivant le signe de cette quantité l’une
2
des deux relations est nécessairement vérifiée :
a + b a + b
f (a)f 60 ou f f (b) 6 0.
2 2
h a+bi ha + b i
Le premier cas assure l’existence d’une racine dans l’intervalle a, , le second dans l’intervalle ,b ,
2 2
ramenant dans chacun des deux cas la recherche à un intervalle d’amplitude moitié moins grande.
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 , m) si f (un )f (m) 6 0 u + vn

avec m = n

(un+1 , vn+1 ) = 
(m, vn ) sinon
 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 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 .

2.2 Mise en œuvre pratique


La recherche déchotomique nécessite quatre paramètres : la fonction f , les valeurs de a et b et la valeur de ε
(qui par défaut sera égal à 10−12 ).

def dicho(f, a, b, epsilon=1e−12):


if f(a) * f(b) > 0:
return None
u, v = a, b
while abs(v − u) > 2 * epsilon:
w = (u + v) / 2
if f(u) * f(w) <= 0:
v = w
else:
u = w
return (u + v) / 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

2.3 Utilisation du module scipy


scipy est un ensemble de modules spécialement dédiés au calcul numérique ; il n’est donc pas étonnant d’y
trouver une fonction permettant de réaliser la recherche dichotomique que nous venons d’étudier. Cette fonction
se nomme bisect et se trouve dans le module scipy.optimize. Comme la majorité des autres fonctions de ce
module ne nous intéressent pas pour l’instant, on se contente de charger en mémoire cette unique fonction :

>>> from scipy.optimize import bisect

Avant de l’utiliser, lisons la documentation qui lui est associée :

>>> help(bisect)

On trouvera figure 4 une version (un peu simplifiée) de ce qu’on obtient.

bisect(f, a, b, xtol=1e−12, maxiter=100)


Find root of a function within an interval.

Basic bisection routine to find a zero of the function f between the


arguments a and b. f(a) and f(b) can not have the same signs.
Slow but sure.

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.

Figure 4 – La page d’aide de la fonction bisect.

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

def dicho(f, a, b, epsilon=1e−12, maxiter=100):


if f(a) * f(b) > 0:
return None
n = 0
u, v = a, b
while abs(v − u) > 2 * epsilon:
n += 1
if n > maxiter:
chn = 'Échec après {} itérations.'.format(maxiter)
raise RuntimeError(chn)
w = (u + v) / 2
if f(u) * f(w) <= 0:
v = w
else:
u = w
return (u + v) / 2

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 :

>>> dicho(sin, 3, 4, maxiter=10)


RuntimeError: Échec après 10 itérations.

3. Valeur approchée d’une intégrale


La calcul d’une intégrale définie de la forme :
Z v
Iu,v (f ) = f (t) dt
u

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.

Méthodes de quadratures composites


Les méthodes de quadratures composites pour calculer une valeur approchée de l’intégrale :
Z b
I(f ) = f (t) dt
a

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

commise est égale à :


n−1
X
En (f ) = Eui ,ui+1 (f )
i=0

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

Figure 5 – Une subdivision de pas régulier avec n = 6.

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.

3.1 Méthode du rectangle


La méthode de quadrature du rectangle est la plus simple qui soit : elle consiste à approcher la fonction f par
la valeur qu’elle prend en un point de l’intervalle [u, v], en général une de ses extrémités. Si on choisit pour
unique nœud x0 = u et pour poids α0 = v − u, ceci conduit à approcher Iu,v (f ) par :

I0u,v (f ) = (v − u)f (u)

y = f (x)

u v

Figure 6 – La méthode du rectangle gauche. La valeur de I0u,v (f ) correspond à l’aire colorée.

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].

Preuve. Si f est un polynôme constant, on pose f (x) = λ et dans ce cas :


Zv
Iu,v (f ) = λ dt = λ(v − u) = (v − u)f (u) = I0u,v (f ),
u

Jean-Pierre Becirspahic
7.8 informatique commune

ce qui montre que la méthode des rectangles est d’ordre 0.


Si f est une fonction de classe C 1 , l’erreur commise vaut :
Zv Z v  Z v
Eu,v (f ) = f (t) dt − (v − u)f (u) = f (t) − f (u) dt donc |Eu,v (f )| 6 |f (t) − f (u)| dt.
u u u

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

• Méthode du rectangle composite


La méthode du rectangle composite consiste à considérer une subdivision (u0 , u1 , . . . , un ) de [a, b] de pas régulier
et à utiliser la linéarité de l’intégrale :
Z b n−1 Z
X uk+1
I(f ) = f (t) dt = f (t) dt
a k=0 uk

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

Figure 7 – Méthode du rectangle composite.

Théorème. — Si f est de classe C 1 , l’erreur de la méthode du rectangle composite vérifie :

(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

3.2 Méthode du point milieu


Una amélioration très simple de la méthode du rectangle consiste à approcher la fonction f par la valeur qu’elle
u+v
prend au point milieu de l’intervalle [u, v], autrement dit à choisir pour unique nœud x0 = = w et pour
2
poids α0 = (v − u), ce qui conduit à approcher Iu,v (f ) par :
I0u,v (f ) = (v − u)f (w)

y = f (x)

u w v

Figure 8 – La méthode du point milieu. La valeur de I0u,v (f ) correspond à l’aire colorée.

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].

Preuve. Si f est un polynôme de degré inférieur ou égal à 1 on pose f (x) = ax + b et on calcule :


Zv
a u+v
 
Iu,v (f ) = (at + b) dt = (v 2 − u 2 ) + b(v − u) = (v − u) a + b = I0u,v (f ).
u 2 2
Si f est une fonction de classe C 2 , l’erreur de quadrature est égale à :
Zv
Eu,v (f ) = f (t) dt − (v − u)f (w).
u
Z v 
L’astuce consiste ici à écrire (v − u)f (w) sous la forme f (w) + (t − w)f 0 (w) dt en jouant sur le fait que
Zv u

(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

• Méthode du point milieu composite


Elle consiste à appliquer la méthode du point milieu à une subdivision de pas régulier du segment [a, b], ce qui
Zb
revient à approcher I(f ) = f (t) dt par :
a

n−1 
b−a X 1b − a
 
f a+ k+ .
n 2 n
k=0

y = f (x)

a b

Figure 9 – Méthode du point milieu composite.

Théorème. — Si f est de classe C 2 , l’erreur de la méthode du point milieu composite vérifie :

(b − a)3
|En (f )| 6 M2
24n2

où M2 est un majorant de |f 00 | sur [a, b].

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

3.3 Méthode du trapèze


La méthode du trapèze consiste à approcher sur [u, v] la fonction f par la fonction affine f˜ joignant les points
(u, f (u)) et (v, f (v)). Il est facile d’obtenir l’expression de f˜ sur [u, v] :

f (v) − f (u) v − x x−u


f˜(x) = f (u) + (x − u) = f (u) + f (v)
v −u v−u v−u
Zv Zv
1 v −x x−u f (u) + f (v)
ce qui conduit à approcher Iu,v (f ) par Iu,v (f ) = f (u) dx + f (v) dx = (v − u) .
u v −u u v −u 2
v −u
Il s’agit donc d’une méthode de quadrature à deux nœuds x0 = u et x1 = v avec les poids α0 = α1 = .
2

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

Figure 10 – La méthode du trapèze. La valeur de I1u,v (f ) correspond à l’aire colorée.

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

• Méthode du trapèze composite


Elle consiste à appliquer la méthode du trapèze à une subdivision de pas régulier du segment [a, b], ce qui
Zb
revient à approcher I(f ) = f (t) dt par :
a
n−1  n−1 n  n−1
b − a X f (uk ) + f (uk+1 ) b − a X b−a X f (b) − f (a)
X  
= f (uk ) + f (uk ) = f (uk ) +
n 2 2n n 2
k=0 k=0 k=1 k=0
n−1 
b−a X b−a b − a f (b) − f (a)

= f a+k + × .
n n n 2
k=0

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 :

Théorème. — Si f est de classe C 2 , l’erreur de la méthode du trapèze composite vérifie :


(b − a)3
|En (f )| 6 M2
12n2
où M2 est un majorant de |f 00 | sur [a, b].

Jean-Pierre Becirspahic
7.12 informatique commune

3.4 La méthode de Simpson


Les méthode du rectangle et du point milieu consistent à approcher f sur [u, v] par un polynôme constant ; la
méthode du trapèze à approcher f par un polynôme de degré inférieur ou égal à 1. Il est logique de poursuivre
cette démarche en approchant maintenant f par un polynôme de degré inférieur ou égal à 2. La théorie de
l’interpolation de Lagrange que vous étudierez en cours de mathématique prouve l’existence d’une unique
fonction polynomiale f˜ de degré inférieur ou égal à 2 vérifiant :
u +v
f˜(u) = f (u), f˜(v) = f (v), f˜(w) = f (w) avec w= .
2
La méthode de quadrature qui en résulte porte le nom
Z de méthode de Simpson ; il s’agit d’une méthode à trois
v
nœuds u, v et w qui consiste à approcher Iu,v (f ) par f˜(t) dt = α0 f (u) + α1 f (v) + α2 f (w).
u

y = f (x)

u w v

Figure 11 – La méthode de Simpson. La valeur de I2u,v (f ) correspond à l’aire colorée.

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

Ainsi, la méthode de Simpson consiste à approcher Iu,v (f ) par :


1 4 1
I2u,v (f ) = (v − u)f (u) + (v − u)f (w) + (v − u)f (v).
6 6 6

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

Si f est de classe C 4 , l’erreur de quadrature est égale à :


Zv
Eu,v (f ) = (f (t) − f˜(t)) dt.
u

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

(x − v)(x − u)(x − w)2


Fixons t ∈ ]u, v[ et considérons la fonction g : x 7→ f (x) − p(x) − K , la constante K étant
24
choisie de sorte que g(t) = 0.
On a g(u) = g(v) = g(w) = g(t) = 0 donc d’après le théorème de Rolle la fonction g 0 possède trois racines entre
ces différentes valeurs. De plus, il se trouve que w est encore racine de g 0 ; la fonction g 0 possède donc au moins
quatre racines, et par application successives du théorème de Rolle on en déduit que g 00 possède au moins trois
racines, g (3) au moins deux racines et g (4) au moins une racine c ∈ ]u, v[.
Sachant que g (4) (c) = f (4) (c) − K on en déduit que K = f (4) (c) et que l’égalité g(t) = 0 peut s’écrire : f (t) − p(t) =
(t − v)(t − u)(t − w)2
f (4) (c) . Ainsi, l’erreur de quadrature vérifie :
24
Zv
(v − t)(t − u)(t − w)2 (v − u)5
|Eu,v (f ) 6 M4 dt = M4 .
u 24 2880

• Méthode de Simpson composite


Elle consiste à appliquer la méthode de Simpson à une subdivision de pas régulier du segment [a, b], ce qui
Zb
revient à approcher I(f ) = f (t) dt par :
a

n−1 !
b−a X 1 4 uk + uk+1 1
 
f (uk ) + f + f (uk+1 ) .
n 6 6 2 6
k=0

Théorème. — Si f est de classe C 4 , l’erreur de la méthode de Simpson composite vérifie :

(b − a)5
|En (f )| 6 M4
2280n4

où M4 est un majorant de |f (4) | sur [a, b].

3.5 Méthodes de Newton-Côtes


Les méthodes que nous venons d’étudier sont des cas particuliers des méthodes de quadrature de Newton-Côtes
basées sur l’interpolation de Lagrange à nœuds équirépartis dans l’intervalle [u, v]. On distingue :
– les formules fermées pour lesquelles les extrémités de l’intervalle [u, v] font partie des nœuds, et parmi
lesquelles se trouvent les méthodes du trapèze et de Simpson ;
– les formules ouvertes pour lesquelles les extrémités de l’intervalle ne font pas partie des nœuds. C’est le
cas de la méthode du point milieu.
On peut montrer que ces méthodes à p + 1 nœuds sont :
– d’ordre p lorsque p est impair (c’est le cas de la méthode des trapèzes) ;
– d’ordre p + 1 lorsque p est pair (c’est le cas des méthodes du point milieu et de Simpson)
ce qui explique pourquoi la méthode des trapèzes n’est pas meilleure que la méthode du point milieu.

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.

3.6 Méthodes de Gauss


Les méthodes de Gauss utilisent une subdivision particulière de l’intervalle [u, v] où les points xi sont racines
d’une certaine famille de polynômes et ne sont pas régulièrement espacés, contrairement aux méthodes
précédentes. Les méthodes de Gauss sont les méthodes les plus répandues et les plus précises, car ces méthodes
sont d’ordre 2p + 1, contre p ou p + 1 pour les méthodes de Newton-Côtes. Nous n’en dirons pas plus car ces
méthodes sont hors programme.

• La fonction quad du module scipy.integrate


Le module scipy.integrate contient une fonction nommée quad qui permet le calcul approché d’une intégrale.
Cette fonction, très efficace, utilise différentes routines (la plupart sont basées sur des méthodes de Gauss) de
manière à obtenir la meilleure approximation possible. Dans son utilisation la plus simple, cette fonction prend
Zb
trois paramètres : f , a et b et renvoie un tuple (I, e) où I est une valeur approchée de l’intégrale I(f ) = f (t) dt
a
et e une estimation de l’erreur |I(f ) − I|.
On notera que les valeurs a et b peuvent éventuellement être égales à ±∞.
>>> import numpy as np
>>> from scipy.integrate import quad

>>> quad(lambda x: np.sin(x), 0, np.pi)


(2.0, 2.220446049250313e−14)

>>> quad(lambda x: np.exp(−x*x), −np.inf, np.inf)


(1.7724538509055159, 1.4202636780944923e−08)
Z π
Le premier exemple calcule une valeur approchée de l’intégrale sin(t) dt = 2 ; le second une valeur approchée
Z +∞ 0
2 √
de l’intégrale de Gauss e−t dt = π.
−∞

>>> 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.

1.2 Ordre de convergence d’une méthode itérative


La vitesse de convergence est un facteur important de la qualité des algorithmes ; si la vitesse de convergence
est élevée, l’algorithme converge rapidement et le temps de calcul est moindre. Ces préoccupations de rapidité
de convergence ont conduit à adopter les définitions suivantes.

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.

Théorème. — La méthode dichotomique est une méthode de résolution numérique d’ordre 1.

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

• Lien entre ordre de convergence et nombre de décimales exactes


Lorsqu’on pose δn = − log10 (en ) on a lim δn+1 − rδn = − log10 (µ) donc pour n assez grand δn+1 ≈ rδn − log10 (µ).
δn mesure approximativement le nombre de décimales exactes de l’approximation xn de c : si δn = p alors
en = 10−p et xn et c partagent les mêmes p premières décimales.
Si r = 1 et µ < 1, l’approximation δn+1 ≈ δn − log10 (µ) traduit le fait qu’à partir d’un certain rang, le nombre
de décimales exactes augmente linéairement avec n (ce qui explique pourquoi les méthodes d’ordre 1 sont
souvent qualifiées de linéaires). Plus précisément, à chaque étape le nombre de décimales exactes est augmenté
de − log10 (µ). Par exemple, dans le cas de la méthode dichotomique on a µ = 1/2 et − log10 (1/2) ≈ 0,3 : toutes les
trois itérations le nombre de décimales exactes est augmenté de 1.

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.

1.3 Critères d’arrêt


En cas de convergence le suite (xn )n∈N construite par une méthode itérative converge vers c. Pour l’utilisation
pratique d’une telle méthode il faut introduire un critère d’arrêt pour interrompre le processus itératif lorsque
l’approximation de c par xn est jugée « satisfaisante ». Pour cela, plusieurs choix sont possibles :
– on peut imposer un nombre maximal d’itérations ;
– on peut imposer une tolérance ε > 0 sur l’incrément : dans ce cas les itérations s’achèvent lorsque
|xn+1 − xn | 6 ε ;
– on peut imposer une tolérance ε > 0 sur le résidu : dans ce cas les itérations s’achèvent lorsque |f (xn )| 6 ε.
Selon les cas, chacun de ces critères peut s’avérer soit trop restrictif, soit trop optimiste.

2. Principales méthodes de résolution numérique


2.1 Méthode dichotomique
Nous avons étudié dans le chapitre précédent la méthode dichotomique. Celle-ci est une méthode qualifiée
de robuste dans le sens où la suite construite (xn )n∈N converge à coup sûr vers un zéro c de la fonction f . En
revanche c’est une méthode plutôt lente : c’est une méthode d’ordre 1, et il faut environ trois itérations pour
gagner une décimale. Cette méthode sera le plus souvent utilisée pour obtenir une première approximation
raisonnable de c avant de poursuivre par une méthode plus rapide mais dont la convergence est uniquement
locale.

2.2 Méthode de la fausse position


Appelée encore méthode regula falsi, il s’agit d’une méthode d’encadrement de c combinant les possibilités de la
méthode de dichotomie avec la méthode de la sécante qui sera introduite plus loin.
Nous allons comme pour la méthode dichotomique considérer une fonction continue f : [a, b] → R vérifiant
f (a)f (b) 6 0, ce qui assure l’existence d’un zéro au moins dans l’intervalle ]a, b[.

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.

2.3 Méthode de Newton-Raphson


Les deux méthodes déjà étudiées : dichotomie et fausse position, déterminent un zéro de f en lequel se produit
un changement de signe en construisant une suite décroissante d’intervalles contenant ce dernier. La méthode de
Newton-Raphson s’affranchit de cette contrainte, en procédant par approximations successives pour construire
une suite (xn )n∈N qui converge en général vers un zéro de f : l’equation f (x) = 0 est remplacée par l’équation
affine ϕn (x) = 0, où ϕn est l’équation de la tangente à la fonction f au point d’abscisse xn .

y = ϕn (x)

c xn+1 xn

y = f (x)

Figure 3 – Le principe de la méthode de Newton-Raphson.

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

Notons M2 un majorant de |f 00 | sur [a, b] et m1 un minorant de |f 0 | sur [a, b].


M
D’après l’inégalité de Taylor-Lagrange, |f (c) − f (x) − (c − x)f 0 (x)| 6 2 (c − x)2 donc :
2
M2
|ϕ(x) − c| 6 (x − c)2 .
2m1
M2
Posons maintenant K = et choisissons un réel η > 0 assez petit pour que Kη < 1 et [c − η, c + η] ⊂ [a, b].
2m1
Pour tout x ∈ [c−η, c+η], |ϕ(x)−c| 6 Kη2 6 η donc ϕ(x) ∈ [c−η, c+η]. Ceci prouve que si on choisit x0 ∈ [c−η, c+η]
la suite (xn )n∈N est bien définie par la relation de récurrence xn + 1 = ϕ(xn ) et vérifie : ∀n ∈ N, xn ∈ [c − η, c + η].
1 n
De plus, |xn+1 − c| 6 K(xn − c)2 et il est dès lors facile d’établir par récurrence que |xn − c| 6 (K|x0 − c|)2 .
K
Puisque K|x0 − c| 6 Kη < 1 ceci montre que lim xn = c.
1 n e
Enfin, en posant en = (K|x0 − c|)2 on a lim n+1 = K > 0 donc la convergence est bien quadratique.
K en2
Remarque. Ce résultat montre que la méthode de Newton-Raphson s’avère particulièrement efficace (c’est
une méthode d’ordre 2) lorsqu’on connait déjà une première approximation de c qu’on cherche à affiner. En
revanche, lorsque x0 n’est pas suffisamment proche de c cette méthode n’offre aucune garantie de convergence.
En outre, lorsque f 0 (c) = 0 la convergence ne peut être garantie, même pour des valeurs très proches de c : le
f (x)
terme 0 , quotient de deux quantités très faibles, engendre des phénomènes de cancellation très importants.
f (x)

• Cas d’une fonction convexe


Le théorème précédent nous assure uniquement d’une convergence locale ; dans le cas particulier des fonctions
convexes on dispose du résultat de convergence globale suivant :

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.

Application : méthode de Héron pour calculer une racine carrée



La méthode de Héron pour calculer une valeur approchée de α lorsque α est un réel strictement positif
consiste à appliquer la méthode de Newton-Raphson à la fonction f : x 7→ x2 − α à partir de la valeur initiale
x0 = α + 1. On a f (x0 ) = α2 + α + 1 > 0 donc le théorème précédent assure la convergence quadratique de la
méthode. Celle-ci consiste donc à itérer la suite définie par :
1 α
 
x0 = α + 1 et xn+1 = xn + .
2 xn

2.4 Dérivée numérique


Dans une mise en œuvre pratique de la méthode de Newton-Raphson se pose le problème du calcul de la
dérivée f 0 (x) en un point. Une solution consiste bien entendu à effectuer le calcul de la dérivée à la main (ou à
l’aide d’un logiciel de calcul formel) mais ce n’est pas toujours chose facile lorsque la fonction f est compliquée.
Une autre solution, que nous allons explorer, consiste à calculer une valeur approchée de f 0 (x).
f (x + h) − f (x)
Nous allons partir d’une idée simple : approcher f 0 (x) par la quantité où h est une quantité très
h
petite. Intuitivement, plus la valeur de h est petite, meilleure est l’approximation. Nous allons constater qu’avec
un ordinateur ceci n’est pas forcément vérifié.

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 :

7,12 − 72 50,4 − 49 1,4


= = = 14,0 car 7,12 = 50,41 est approché par 50,4.
0,1 0,1 0,1

– Si on prend h = 0,01 on approche f 0 (7) = 14 par :

7,012 − 72 49,1 − 49 0,1


= = = 10,0 car 7,012 = 49,140 1 est approché par 49,1.
0,01 0,01 0,01

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

2.5 Méthode de la sécante


Une autre solution si on ne souhaite pas à avoir à se préoccuper du calcul de la dérivée dans la méthode de
Newton-Raphson consiste à remplacer cette dernière par la pente de la corde reliant les points d’abscisses xn−1
et xn−2 . Autrement dit, la relation :
f (x )
xn+1 = xn − 0 n
f (xn )
est remplacée par la relation :
xn − xn−1
xn+1 = xn − f (xn )
f (xn ) − f (xn−1 )

xn−1 f (xn ) − xn f (xn−1 )


qui peut aussi s’écrire xn+1 = .
f (xn ) − f (xn−1 )
Le point de coordonnées (xn+1 , 0) apparaît maintenant comme l’intersection avec l’axe des abscisses de la corde
reliant les points de coordonnées (xn−1 , f (xn−1 )) et (xn , f (xn )).

c
xn+1 xn xn−1

Figure 4 – Le principe de la méthode de la sécante.

Notons que cette méthode nécessite le choix de deux valeurs initiales x0 et x1 .


Nous admettrons le résultat suivant :

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

2.6 Utilisation du module scipy.optimize


Dans le chapitre précédent, nous avons appris que dans le module scipy.optimize se trouve une fonction
nommée bissect qui applique la méthode dichotomique pour trouver un zéro d’une fonction. Dans ce même
module se trouve une fonction nommée newton qui, comme son nom l’indique, applique la méthode de
Newton-Raphson. Examinons-en la page d’aide (légèrement simplifiée) :
newton(func, x0, fprime=None, tol=1.48e−08, maxiter=50)
Find a zero using the Newton−Raphson or secant method.

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

Figure 5 – Le graphe de la fonction f : x 7→ 2x3 − 4x − 1 sur [−2, 2].

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

>>> def f(x): return 2 * x**3 − 4 * x − 1


>>> def df(x): return 6 * x**2 − 4

>>> newton(f, −1, df)


−1.2670350983613659

>>> newton(f, 0, df)


−0.25865202250415276

>>> newton(f, 1, df)


1.5256871208655185

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

>>> newton(f, 0, df)


RuntimeError: Failed to converge after 50 iterations, value is 0.0

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

Figure 1 – Décharge du condensateur.

1.2 Méthode d’Euler explicite


Les méthodes que nous allons étudier consistent à subdiviser l’intervalle de temps [t0 , t0 + T] en n + 1 points
t0 < t1 < · · · < tn = t0 + T puis à approcher la relation :
Z tk+1 Z tk+1
0
x(tk+1 ) − x(tk ) = x (t) dt = f (x(t), t) dt.
tk tk

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 :

∀k ∈ ~0, n − 1, xk+1 = xk + hk f (xk , tk )

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

Figure 2 – Méthode d’Euler explicite avec n = 4 puis avec n = 10.

notablement atténuer la divergence de la solution numérique de la solution analytique. Cependant, dans de


nombreux cas la méthode d’Euler procure des résultats acceptables (au moins qualitativement). On peut en
observer un exemple avec la réponse du circuit RC à un signal d’entrée sinusoïdal figure 3.

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.

1.3 Méthode d’Euler implicite


Z tk+1
La méthode d’Euler implicite consiste à approcher l’intégrale f (x(t), t) dt par la méthode du rectangle droit,
tk
ce qui conduit à définir la suite (x0 , x1 , . . . , xn ) par les relations :
xk+1 = xk + hk f (xk+1 , tk+1 ).
On observe que cette relation ne procure pas une relation explicite de xk+1 puisque ce terme est aussi présent
dans le second membre. Pour calculer ce terme il est souvent nécessaire de coupler cette méthode à une méthode
de résolution numérique des équations telle la méthode de Newton-Raphson (voir le chapitre 8). Pour cette
raison, elle se révèle plus coûteuse à mettre en œuvre.
Dans la pratique, la méthode d’Euler implicite se révèle souvent plus stable que la méthode explicite : elle est
moins précise à court terme, mais diverge moins rapidement de la solution exacte que la méthode explicite. On

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

Figure 4 – Comparaison des méthodes d’Euler explicite et implicite.

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.

1.4 Schéma numérique


On appelle schéma numérique explicite à un pas toute équation de récurrence de la forme :

∀k ∈ ~0, n − 1, xk+1 = xk + hk φ(xk , tk , hk )

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

Figure 5 – L’erreur de consistance de la méthode d’Euler.


p+1
Un schéma numérique de pas régulier est dit d’ordre p lorsque ek+1 = O(hk ) lorsque hk tend vers 0.
n
X
Une méthode numérique est dite consistante lorsque : lim ek = 0.
n→+∞
k=1

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.

Preuve. Si f est de classe C 1 alors x est de classe C 2 et d’après la formule de Taylor-Young,

x(tk+1 ) = x(tk + hk ) = x(tk ) + hk x0 (tk ) + O(h2k ) = x(tk ) + hk f (x(tk ), tk ) + O(h2k )

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

2. Les méthodes de Runge-Kutta


2.1 La méthode du point milieu
Notons Mn le point de coordonnées (tn , x(tn )). Comme le montre la figure ci-dessous, le segment [Mn , Mn+1 ] a
en général une pente plus proche de x0 (tn + hn /2) (pente de la tangente au point milieu) que de x0 (tn ), pente de
la tangente en Mn utilisée dans la méthode d’Euler.

Mn+1

Mn

point en lequel φ(x, t, h) est évalué

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

Figure 7 – Comparaison entre les méthodes d’Euler et du point milieu.

2.2 Les méthodes de Runge-Kutta


Nous allons maintenant généraliser la démarche que nous venons de suivre pour la méthode du point milieu.
On appelle méthode de Runge-Kutta explicite à s étages (en abrégé RKs ) une méthode définie par le schéma :
k1 = f (x, t)
k2 = f (x + (a21 k1 )h, t + c2 h)
..
.
 X s−1  
ks = f x + asi ki h, t + cs h
i=1
s
X
φ(x, t, h) = bi ki
i=1

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

Méthode d’Euler (ordre 1) Méthode du point milieu (ordre 2)

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

Méthode de Heun (ordre 2) Méthode RK4 (ordre 4)

Figure 8 – Schémas de Runge-Kutta classiques.

Par exemple, le schéma de Heun est le suivant :

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.

3. Systèmes différentiels et équations d’ordre supérieur


3.1 Systèmes différentiels
Considérons maintenant un système de p équations différentielles liant p fonctions numériques x1 , . . . , xp et
prenant la forme suivante :
 0
 x1 = f1 (x1 , x2 , . . . , xn , t)

0

 x2 = f2 (x1 , x2 , . . . , xn , t)






 ... = ................

 x0 = f (x , x , . . . , x , t)

p p 1 2 n

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

La fonction vectorielle X vérifie l’équation différentielle suivante :

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 :

Xk+1 = Xk + hk F(Xk , tk ) avec hk = tk+1 − tk


 h 
ik+1 = ik + k −Rik − sk + e(tk )
 

L


ce qui dans le cas du circuit RLC revient à écrire : 

i
 sk+1 = sk + hk k



C

4
tension aux bornes du condensateur
3

4
0.000 0.005 0.010 0.015 0.020

Figure 9 – Réponse d’un circuit RLC à une sollicitation sinusoïdale.

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

3.2 Équations différentielles d’ordre 2


Considérons maintenant une équation différentielle scalaire d’ordre 2, c’est-à-dire de la forme :
x00 = f (x, x0 , t)
La méthode consiste ici à se ramener à un système différentiel en considérant le vecteur X = (x, x0 ). Ce dernier
est en effet solution du système différentiel :
( 0
x =y    
0 soit X0 = F(X, t) avec F (x, y), t = y, f (x, y, t) .
y = f (x, y, t)
Considérons par exemple constitué d’une masse m liée à un point fixe par un fil de longueur `, et oscillant dans
le champ de pesanteur terrestre. En notant θ l’angle que fait le fil avec la verticale, on obtient l’équation du
mouvement :
d2 θ g
+ ω2 sin(θ) = 0 avec ω2 = .
dt 2 `
Pour résoudre numériquement cette équation, on se ramène au système différentiel suivant :

 dθ

 = θ̇
 dt


dθ̇


= −ω2 sin(θ)



dt

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.

4. Utilisation de la fonction odeint


Les graphes des figures 9 et 10 n’ont pas été obtenus par la méthode d’Euler (qui donne souvent des résultats
médiocres lorsqu’il s’agit de résoudre un système différentiel) mais à l’aide de la fonction odeint du module
scipy.integrate.
Résolution numérique des équations différentielles 9.11

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.

Solve a system of ordinary differential equations:

dy/dt = func(y,t0)

where y can be a vector.

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).

Résolution d’une équation scalaire


Le script qui a été utilisé pour obtenir le graphe de la figure 3 ressemble à :
def f(s, t):
return (e(t) − s) / tau
ds 1 q
t = np.linspace(0, .01, 200) = (e(t) − s) avec s(0) = 0 .
s = odeint(f, q0/C, t) dt τ C

plot (t, s)

Résolution d’un système différentiel


Le script qui a été utilisé pour obtenir le graphe de la figure 9 ressemble à :
def f(x, t):
[i, s] = x
return [(−R*i−s−e(t))/L, i/C]
 di 1
= (−Ri − s − e(t))


 dt L


t = np.linspace(0, .02, 200) avec i(0) = 0 et s(0) = 0.
ds i


x = odeint(f, [0, 0], t)


 =
dt C
plot (t, x[:, 1])

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]).

Résolution d’une équation différentielle du second ordre


Le script qui a été utilisé pour obtenir le graphe de la figure 10 ressemble à :
def f(x, t):
[theta, dtheta] = x
return [dtheta, −omega**2*sin(theta)]

 dθ

 = θ̇
 dt


t = np.linspace(0, 30, 200) avec θ(0) = θ0 et θ̇(0) = 0.
dθ̇


x = odeint(f, [theta0, 0], t) = −ω2 sin(θ)



dt

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.8 Diagramme des phases

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

Figure 11 – Diagramme des phases du pendule pesant.


informatique commune
Chapitre 10
Résolution numérique d’un
système linéaire

1. Python et le calcul matriciel


Le module numpy contient les éléments indispensables à la modélisation des vecteurs, matrices et plus généra-
lement des tableaux multidimensionnels. Pour cela, numpy fournit le type ndarray, qui, bien que très proche sur
le plan syntaxique du type list , diffère de ce dernier sur plusieurs points importants :
– la taille des tableaux numpy est fixée au moment de la création et ne peut plus être modifiée par la suite 1 ;
– les tableaux numpy sont homogènes, c’est-à-dire constitués d’éléments de même type.
En contrepartie, l’accès aux éléments d’un tableau numpy est incomparablement plus rapide, ce qui justifie
pleinement leur usage pour manipuler des matrices de grandes tailles.
Par la suite, nous supposerons avoir écrit au début de chacun des scripts de ce chapitre l’instruction :
import numpy as np

ce qui nous permettra de visualiser aisément les functions de ce module : elles seront préfixées par .np.

1.1 Création de tableaux


On utilise en général la fonction array pour former un tableau à partir de la liste de ses éléments (ou de la
liste des listes pour une matrice bi-dimensionnelle). Par exemple, pour créer une matrice 3 × 4 à partir de ses
éléments on écrira :
>>> 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]])

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

Les types autorisés sont les suivants :

bool (booléens), int (entiers), float (flottants), complex (complexes)

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.

Pourquoi tant de types différents ?


Prenons le cas de la représentation matricielle d’une image non compressée 1600 × 1200, chaque pixel étant
représenté par un triplet RGB permettant de reconstituer une couleur par synthèse additive. Autrement dit,
avec numpy une image est modélisée par un tableau tri-dimensionnel 1600 × 1200 × 3.
Chaque composante primaire est décrite par un entier non signé codé sur 8 bits ( = 1 octet), autrement dit un
entier de l’intervalle ~0, 28 − 1 = ~0, 255. Avec le data type uint8 (entier non signé codé sur 8 bits) l’espace
mémoire utilisé pour représenter cette image vaut 1600 × 1200 × 3 = 5 760 000 octets soit 5,5 Mio. Si on utilisait
pour chacune des composantes des entiers codés sur 64 bits ( = 8 octets) l’espace mémoire nécessaire serait huit
fois plus important, soit 44 Mio.

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.])

>>> a[:, 2] # 3e colonne de la matrice a


array([ 2., 5., 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

>>> a2 = a[2].copy() # a2 est une copie de la 3e ligne de a

>>> a[2, 0] = 7

>>> a1 # la modification de a se répercute sur a1


array([ 7., 6., 1., −1.])

>>> 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[0], a[1] = a[1], a[0]

>>> 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[[i, j]] = a[[j, i]]

et d’inverser les colonnes i et j de ce tableau par :

a[:, [i, j]] = a[:, [j, i]]

1.3 Redimensionnement d’un tableau


La méthode shape permet de connaître les dimensions d’un tableau de type ndarray :

>>> 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 = np.array([1, 2, 3], dtype=float)


>>> a # a est un vecteur
array([ 1., 2., 3.])

>>> 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.

a : shape=3,4 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12.


1re ligne 2e ligne 3e ligne

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).

• Création de tableaux spécifiques


La fonction zeros permet de former des tableaux dont tous les coefficients sont nuls : le premier et seul
argument obligatoire est un tuple qui précise la dimension du tableau 2 et un argument facultatif (par défaut
égal à float) permet de fixer le type de données.
>>> np.zeros((2, 3), dtype=int)
array([[0, 0, 0],
[0, 0, 0]])

La fonction identity construit la matrice d’identité d’ordre n :


>>> np.identity(3)
array([[ 1., 0., 0.],
[ 0., 1., 0.],
[ 0., 0., 1.]])

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

>>> np.diag([1., 2., 3.])


array([[ 1., 0., 0.],
[ 0., 2., 0.],
[ 0., 0., 3.]])

1.4 Opérations élémentaires sur les lignes et les colonnes


Les fonctions usuelles qui sont définies au sein de numpy peuvent être appliquées à des tableaux : dans ce cas,
chacun des coefficients de ce dernier se voient appliquer la fonction. C’est aussi le cas des opérateurs binaires :
si a = (aij ) et b = (bij ) sont deux matrices n × p et ⊕ un opérateur binaire alors a ⊕ b calcule la matrice (aij ⊕ bij ).
Ainsi, pour calculer l’addition de deux matrices a et b il suffira d’écrire a + b. De même, pour calculer le
produit d’un scalaire t par une matrice a il suffira d’écrire t * a. En revanche, on se gardera bien de calculer
le produit matriciel en écrivant a * b car ce qui est ainsi calculé est le produit terme à terme de chacun des
coefficients de ces deux matrices. Pour calculer le produit matriciel, il faut utiliser la fonction dot(a, b).
On notera que si a est une matrice et x un vecteur on calcule le produit ax à l’aide de la fonction dot.
Ces opérations vont nous permettre d’appliquer aisément des opérations élémentaires sur les lignes d’une
matrice :
– L’opération Li ← Li + tLj (ajout de tLj à la ligne Li ) s’écrit :

a[i] = a[i] + t * a[j]

– L’opération Li ← tLi (multiplication de Li par t) s’écrit :


a[i] = t * a[i]

– L’opération Li ↔ Lj (permutation des lignes Li et Lj ) s’écrit 3 :

a[[i, j]] = a[[j, i]]

On prendra bien garde au fait que chacune de ces trois opérations élémentaires modifie physiquement la
matrice a.

2. Méthode du pivot de Gauss


La méthode du pivot de Gauss est une méthode générale de résolution d’un système linéaire de la forme : Ax = b,
où A est une matrice inversible. Elle repose sur l’observation faite qu’appliquer une opération élémentaire (Oi )
sur les lignes d’une matrice équivaut à multiplier cette dernière à gauche par une certaine matrice inversible Ui .
Compte tenu de l’équivalence :
Ax = b ⇐⇒ Ui Ax = Ui b
on constate qu’on ne modifie pas l’ensemble des solutions d’une équation linéaire en appliquant les mêmes
opérations élémentaires sur les lignes de A et de b.
La méthode du pivot de Gauss comporte trois étapes :
1. une première étape dite de descente qui consiste à transformer la matrice inversible A en une matrice A0
triangulaire supérieure tout en effectuant les mêmes opérations sur b ;
       
       
  x =   ⇐⇒   x =  
       

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.

3. Relire le paragraphe consacré au fancy indexing.

http://info-llg.fr
10.6 informatique commune

2.1 L’étape de descente


Celle-ci consiste à transformer progressivement la matrice A par opérations élémentaires sur les lignes tout en
maintenant l’invariant :
a11 a12 . . . . . . . . . . . . . a1n 
 
 0 a
22 . . . . . . . . . . . . . a2n 

 
 . .. 
 . .. ..
 .
 . . . 
à l’entrée de la j e boucle, A =  .
 0 · · · 0 ajj · · · ajn 
 .. .. .. .. 
 
 . . . . 
 
0 · · · 0 anj · · · ann

Dans le cas où la matrice A est de la forme ci-dessus, on procède en deux temps :


1. on recherche un pivot non nul 4 aij parmi ajj , aj+1,j , . . . , anj puis on permute les lignes Li et Lj : Li ↔ Lj ;
2. à l’issue de cette première étape nous sommes assurés que désormais ajj , 0 ; on l’utilise comme pivot
pour remplacer tous les termes aij pour i > j par des zéros à l’aide de l’opération élémentaire :
aij
∀i ∈ ~j + 1, n, Li ← Li − Lj .
ajj

• 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 :

3,000 · 10−3 5,914 · 101 x 5,917 · 101


( ! ! !
0,003 x + 59,14 y = 59,17
⇐⇒ =
5,291 x − 6,13 y = 46,78 5,291 · 100 −6,130 · 100 y 4,678 · 101

dont la solution exacte est x = 10, y = 1.


Choisir 0, 003 pour pivot conduit à effectuer l’opération élémentaire : L2 ← L2 − λL1 avec
5,291
λ= = 1 763,666 · · · ≈ 1,764 · 103
0,003
et conduit à résoudre le nouveau système :

3,000 · 10−3 5,914 · 101 5,917 · 101


! ! !
x
=
0 −1,043 · 105 y −1,044 · 105

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 :

3,000 · 10−3 −2,000 · 10−2


! ! !
0 x
=
0 −1,043 · 105 y −1,044 · 105

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.

• Programmation de l’étape de descente


a) Rédiger une fonction recherche_pivot(A, b, j) qui détermine le coefficient aij le plus grand en module
parmi ajj , . . . , anj puis qui permute les lignes Li et Lj de A et de b.
b) Rédiger une fonction elimination_bas(A, b, j) qui effectue les éliminations successives des coefficients
situés sous ajj , en supposant ce coefficient non nul. On effectuera en parallèle les mêmes opérations sur b.
c) En déduire une fonction descente(A, b) qui par opérations élémentaires sur les lignes des matrices A et b
réalise l’étape de descente de la méthode du pivot de Gauss.

2.2 L’étape de remontée


Celle-ci intervient lorsque la matrice A est triangulaire supérieure, les coefficients de la diagonale étant non
nuls. On transforme progressivement la matrice A en maintenant l’invariant :
a11 · · · a1j 0 · · · 0 
 
 .. .. .. .. 
 0 . . . . 

 .. .. 

. .. a
e
 .
jj 0 . 
à l’entrée de la (n − j + 1) boucle, A =   .
.. .. 
 0 · · · 0 aj + 1, j + 1 . .


 . . . . 
 . .. .. . . 0 
 .
 
0 ··· 0 ··· 0 ann

http://info-llg.fr
10.8 informatique commune

et en utilisant ajj comme pivot pour éliminer les coefficients a1j , . . . , aj−1,j .

• Programmation de l’étape de remontée


a) Rédiger une fonction elimination_haut(A, b, j) qui effectue les éliminations successives des coefficients
situés au-dessus de ajj , en supposant ce coefficient non nul. On effectuera en parallèle les mêmes opérations sur
b.
b) En déduire une fonction remontee(A, b) qui par opérations élémentaires sur les lignes des matrices A et b
réalise l’étape de remontée de la méthode du pivot de Gauss.

2.3 Résolution du système linéaire


La troisième et dernière étape de la méthode consiste à résoudre le système diagonal obtenu après les deux
étapes précédentes.

• Programmation de la méthode du pivot de Gauss


a) Rédiger une fonction solve_diagonal(A, b) qui prend en arguments une matrice diagonale inversible A
et un vecteur b et qui retourne l’unique vecteur x solution de l’équation Ax = b.
b) En déduire une fonction gauss(A, b) qui retourne l’unique solution d’un système de Cramer Ax = b. On
travaillera sur des copies des matrices A et b pour éviter de modifier physiquement ces dernières.

Étude de la complexité temporelle de la méthode


Les différentes fonctions qui ont été écrites ont pour coûts temporels respectifs :
– recherche_pivot a un coût en O(n) ;
– elimination_bas a un coût en O(n2 ) ;
– descente a un coût en O(n3 ) ;
– elimination_haut a un coût en O(n2 ) ;
– remontee a un coût en O(n2 ) ;
– solve_diagonal a un coût en O(n) ;
La méthode de Gauss dans son ensemble a donc un coût temporel en O(n3 ), à quoi s’ajoute un coût spatial en
O(n2 ) si on travaille sur des copies des matrices A et B.

2.4 Applications de la méthode du pivot de Gauss


Calcul du déterminant
Une fois l’étape de descente terminée, la matrice obtenue A0 est triangulaire donc le calcul de son déterminant
est chose aisée. De plus, son déterminant est égal à (−1)k det A où k est le nombre d’échanges qui ont été effectués
entre deux lignes. Ceci fournit donc une méthode de calcul du déterminant de A.

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.5 Utilisation du module numpy.linalg


Le module numpy.linalg contient un certain nombre de fonctions dédiées à l’algèbre linéaire, en particulier
une fonction solve qui joue le même rôle que la fonction gauss que nous venons d’écrire. Celle-ci va nous
permettre de tester notre fonction pour résoudre le système :

 2 4 −4 1  x1   0 
    
 3 6 1 −2 x2  −7
  =  

−1 1 2 3  x3   4 

    
1 1 −4 1 x4 2

>>> from numpy import linalg as la

>>> A = np.array([[2, 4, −4, 1],


[3, 6, 1, −2],
[−1, 1, 2, 3],
[1, 1, −4, 1]], dtype=float)

>>> b = np.array([0, −7, 4, 2], dtype=float)

>>> 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

Annexe : la méthode du pivot partiel de Gauss

def recherche_pivot(A, b, j):


p = j
for i in range(j+1, A.shape[0]):
if abs(A[i, j]) > abs(A[p, j]):
p = i
if p != j:
b[[p, j]] = b[[j, p]]
A[[p, j]] = A[[j, p]]

def elimination_bas(A, b, j):


for i in range(j+1, A.shape[0]):
b[i] = b[i] − (A[i, j] / A[j, j]) * b[j]
A[i] = A[i] − (A[i, j] / A[j, j]) * A[j]

def descente(A, b):


for j in range(A.shape[1] − 1):
recherche_pivot(A, b, j)
elimination_bas(A, b, j)

def elimination_haut(A, b, j):


for i in range(j):
b[i] = b[i] − (A[i, j] / A[j, j]) * b[j]

def remontee(A, b):


for j in range(A.shape[1] − 1, 0, −1):
elimination_haut(A, b, j)

def solve_diagonal(A, b):


for k in range(b.shape[0]):
b[k] = b[k] / A[k, k]
return b

def gauss(A, b):


U = A.copy()
v = b.copy()
descente(U, v)
remontee(U, v)
return solve_diagonal(U, v)

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.

Interaction avec le système d’exploitation


Dans le premier chapitre de ce cours, nous avons appris qu’un système d’exploitation gère les fichiers et
répertoires sous forme arborescente, et qu’au sein de cette arborescence se trouve le répertoire courant, c’est-à-
dire celui dans lequel seront par défaut sauvegardés et recherchés les fichiers que nous manipulerons. Il se peut
que ce répertoire ne corresponde pas à celui dans lequel vous souhaitez travailler ; il est alors nécessaire de
préciser à l’interprète python le chemin de votre répertoire de travail.
Les instructions permettant à l’interprète de dialoguer avec le système d’exploitation ne sont pas directement
accessibles ; elles font partie d’un module additionnel, nommé os, qu’il va falloir commencer par importer :
>>> import os

Obtenir le répertoire courant


La première commande de ce module que nous allons utiliser est la fonction getcwd(), qui indique le répertoire
courant :
>>> os.getcwd()
'/home/bob'

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).

Changer le répertoire courant


La fonction chdir permet de changer le répertoire courant, en indiquant en argument (sous la forme d’une
chaîne de caractères) le nouveau répertoire souhaité. Quel que soit votre système d’exploitation, vous pouvez
utiliser les conventions lexicales du monde unix, le module os se chargeant automatiquement de la conversion.
Si on reprend l’exemple simplifié donné au chapitre 1 de ce cours, on fait du répertoire travail de Bob le
répertoire courant en écrivant :
>>> os.chdir('/home/bob/travail')

Obtenir la liste des fichiers et répertoires


Enfin, la fonction listdir permet d’obtenir la liste des fichiers et répertoires contenus dans un répertoire
dont le nom a été passé en argument (sous la forme d’une chaîne de caractères). Là encore, quel que soit votre
système d’exploitation, vous pouvez utiliser les conventions lexicales du monde unix. Poursuivons le même
exemple, en visualisant le contenu du répertoire courant :
>>> os.listdir('.')
['doc1']

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.

1. Lecture et écriture dans un fichier texte


Désormais, nous supposerons que dans le répertoire courant se trouve un fichier nommé exemple.txt contenant
le texte suivant :
Am, stram, gram,
Pic et pic et colégram,
Bour et bour et ratatam,
Am, stram, gram.

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é.

1.1 Lecture complète ou partielle


L’objet que nous venons de créer est ce qu’on appelle un flux : les caractères sont lisibles uniquement les uns
après les autres, sans possibilité de retour en arrière ni de saut en avant. Ce n’est guère pratique, aussi notre
première tâche sera de convertir ce flux en chaîne de caractères.
Le moyen le plus simple est d’utiliser la méthode read(), prise sans argument, qui lit le flux dans son entier et
le convertit en chaîne de caractères :
>>> comptine.read()
'Am, stram, gram,\nPic et pic et colégram,\nBour et bour et ratatam,\nAm,
stram, gram.'

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

• Lecture par lignes


Plus intéressant, la méthode readline() permet de lire une ligne de texte (en incluant le caractère de fin de
ligne), et surtout la méthode readlines(), qui fournit la liste des lignes du texte 2 :
>>> comptine = open('exemple.tex', 'r')
>>> comptine.readlines()
['Am, stram, gram,\n', 'Pic et pic et colégram,\n', 'Bour et bour et
ratatam,\n', 'Am, stram, gram.']

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.

Fermeture d’un fichier


Enfin, une fois le fichier lu, n’oublions pas de le refermer, afin qu’il soit disponible de nouveau pour tout autre
usage : c’est le rôle la méthode close() :
>>> comptine.close()

1.2 Fichiers CSV


De nombreuses données scientifiques se présentent sous forme de tableaux ; une façon simple de les transmettre
est de les représenter par un fichier CSV (pour comma-separated value) : il s’agit d’un simple fichier texte, chaque
ligne de texte correspondant à une ligne du tableau et un caractère spécial (virgule ou point-virgule le plus
souvent) aux séparations entre les colonnes. Par exemple, le tableau suivant :
Planète rayon (en km) gravité (en m/s2 ) période de révolution (en jours)
Mercure 2439 3, 7 88
Vénus 6052 8, 9 225
Terre 6378 9, 8 365
Mars 3396 3, 7 687

sera représenté par le fichier planetes.csv contenant le texte suivant 3 :


Mercure, 2439, 3.7, 88
Vénus, 6052, 8.9, 225
Terre, 6378, 9.8, 365
Mars, 3396, 3.7, 687

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(','))

À cette étape, tab est une liste de listes égale à :

[['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])

et la liste tab est maintenant prête à être utilisée :

[['Mercure', 2439, 3.7, 88], ['Vénus', 6052, 8.9, 225],


['Terre', 6378, 9.8, 365], ['Mars', 3396, 3.7, 687]]

1.3 Écrire dans un fichier


Deux modes d’ouverture sont possibles pour écrire dans un fichier : le mode 'w' (write pour écrire) et le mode
'a' (append pour ajouter). Le premier crée un nouveau fichier (s’il existe déjà un fichier du même nom, ce
dernier sera effacé) et l’écriture commencera au début du fichier, tandis que le second ajoutera à la suite des
données existantes celles que nous allons lui fournir. Dans les deux cas, la méthode write permet d’enregistrer
les chaînes de caractères passées en argument les unes à la suite des autres.
Par exemple, pour ajouter au fichier planetes.csv des données supplémentaires, on procède ainsi :

>>> planetes = open('planetes.csv', 'a')


>>> planetes.write('Jupiter, 71492, 24.8, 4335\n')
>>> planetes.write('Saturne, 60268, 10.4, 10757\n')
>>> planetes.close()

(Ne pas oublier de fermer le fichier pour enregistrer les modifications.)

2. Encodage d’un fichier texte


2.1 Le jeu de caractères ascii
À l’origine du développement de l’informatique, il a été décidé de coder un caractère sur un octet, et plus
précisément sur 7 bits, le premier bit étant fixé à 0. On disposait ainsi de 27 = 128 caractères différents, qui
constituent le jeu de caractères ascii (American Standard Code for Information Interchange). Tout nombre compris
entre 0 et 127 s’écrivant en base 16 sous la forme (xy)16 avec x ∈ {0, 1, . . . , 6, 7} et y ∈ {0, 1, . . . , e, f}, on représente
souvent la correspondance entre code ascii et caractères par le tableau de la figure 1.
Les cases grisées correspondent à des caractères spéciaux (tabulation, passage à la ligne, etc.) qui peuvent
dépendre du système ; le caractère correspondant au code ascii (20)16 = 32 est l’espace inter-mot.

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'

En effet, 65 = (41)16 et 97 = (61)16 .


Manipulation de fichiers 11.5

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 { | } ˜

Figure 1 – Table des caractères ascii.

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 ð ñ ò ó ô õ ö ÷ ø ù ú û ü ý þ ÿ

Figure 2 – Extention latin-1 de la norme ascii.

2.2 La norme Unicode


Ces différentes contraintes ont poussé à l’adoption d’une norme universelle, la norme Unicode, qui attribue
un identifiant numérique différent à chacun des milliers de caractères nécessaires à la transcription des
différentes langues mondiales. En revanche, il importe de comprendre que cette norme ne précise pas sous
quelle forme cet identifiant doit être encodé par le système informatique. Il existe donc plusieurs normes
d’encodages différentes en fonction des besoins, mais chacune a en commun d’associer à chaque caractère le
même identifiant numérique.
L’encodage le plus fréquent est l’utf-8 ; c’est la norme utilisée par défaut par Python. Elle utilise entre 1 et
4 octets pour encoder un caractère et présente l’avantage d’assurer une parfaite compatibilité avec les textes
encodés en ASCII.
Revenons un instant sur le fichier exemple.txt qui contient la comptine utilisée au début de ce chapitre ; il
s’agit d’un fichier encodé au format utf-8, encodage qui a été choisi par défaut par Python lors de l’ouverture :

Jean-Pierre Becirspahic
11.6 informatique commune

>>> comptine = open('exemple.txt', 'r')


>>> print(comptine.read())
Am, stram, gram
Pic et pic et colégram,
Bour et bour et ratatam,
Am, stram, gram.

Essayons maintenant de l’ouvrir avec un mauvais encodage :


>>> comptine = open('exemple.txt', 'r', encoding='latin1')
>>> print(comptine.read())
Am, stram, gram
Pic et pic et colégram,
Bour et bour et ratatam,
Am, stram, gram.

On peut observer que le caractère non ASCII "é" a été mal interprété à cause du mauvais encodage.

Caractères non occidentaux


À titre de curiosité, notons pour finir qu’il est possible d’accéder aux caractères non latins à l’aide de la fonction
chr dont nous avons déjà parlé, qui non seulement retourne le caractère ASCII dont l’identifiant a été passé en
paramètre mais plus généralement le caractère unicode associé à son identifiant, pour peu que le système soit
capable de l’afficher.
Par exemple, les caractères minuscules de l’alphabet grec ont un identifiant compris entre 945 et 969 ; le script
qui suit permet de les afficher :
>>> for i in range(945, 970):
... print(chr(i), end=' ')
αβγδεζηθικλµνξoπρςστυϕχψω

3. Lecture et écriture dans un fichier image


Les images publiées sur un site internet, les photographies prises avec un téléphone portable, sont des exemples
d’images numériques. Il est possible de représenter ce type d’image par une matrice. Par exemple, L’image
ci-dessous peut être représentée par une matrice 35 × 35 dont les éléments, des 0 ou des 1, indiquent la couleur
du pixel : 0 pour le noir et 1 pour le blanc.
11111101111111111111111101111111111
11111000111111111111111000111111111
11111000011111111111111000111111111
11111000001111111111110000111111111
11111000000111111111100000011111111
11111000000010000000000000000011111
11111000000000000000000000000011111
11111000000000111111110000110011111
11111000000001111111111000111001111
11111000000011111111111100111100111
11110000000011111111111100111110111
11110000000111111111111110111111011
11110000000111111111111110111001001
11110000001111111110001110111000101
11100000001111111100001110111000100
11100000001111111100001110011000100
11000000001111111100001110011000100
10000000001111111100001110011100110
10000000000111111110001110001111110
00000000000111111111111100101111110
10000000000011111111111101100000100
11000001110001111111111001000000010
11100011111100011111110011000000100
11000011100011000000000111000000000
10000011100011110000111111100000000
11110011111001111111111111111110101
11111001111110001111111111111001001
11111001111110000000111100000011011
11111100111111000000000000000010111
11111110011111000111000000000100111
11111111001111101111110111001001111
11111111100111110011111110010011111
11111111110001111000000000000111111
11111111111100000111110000011111111
11111111111111000000000001111111111

Figure 3 – Une image binaire et la matrice qui lui est associée.

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

Figure 4 – Huit niveaux de gris différents.

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.

Traitement d’images en Python


À moins d’installer sur votre système la bibliothèque Pillow spécialisée dans le traitement d’image sous Python
il faudra se contenter du module matplotlib.image qui propose quelques fonctions basiques pour importer
une image, essentiellement :
– imread('fichier.png') qui prend en argument le nom d’un fichier image au format png et retourne un
tableau numpy ;
– imsave('fichier.png', tableau) qui sauvegarde un tableau numpy représentant une image sous la
forme d’un fichier png.
À ces deux fonctions il peut être intéressant d’associer la fonction imshow du module matplotlib.pyplot qui
affiche à l’écran une représentation imagée d’un tableau numpy.

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]

fig = plt.figure(figsize=(6, 6), frameon=False)


plt.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=.05, wspace=.05)

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.

Les principes et l’architecture d’une base de données


Un système de gestion de bases de données (SGBD) est un logiciel qui organise et gère les données de façon
transparente pour l’utilisateur. Ce sont des logiciels dont la conception est bien trop complexe pour pouvoir être
abordée dans ce cours ; nous nous contenterons d’interagir avec eux par l’intermédiaire de requêtes exprimées
dans un langage devenu standard au fil des temps : le langage SQL (pour Structured Query Language). La
majorité des SGBD comprend au moins un sous-ensemble des vocables SQL, agrémenté d’un certain nombre
d’expressions qui leur sont spécifiques.

• 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.

1. De l’anglais tier, qui signifie niveau.

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

Figure 1 – Architecture à deux niveaux Figure 2 – Architecture à trois niveaux

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/

Figure 3 – L’interface client de la BDR mondial.

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.

NAME CODE CAPITAL PROVINCE AREA POPULATION


France F Paris Ile de France 547030. 64933400
Spain E Madrid Madrid 504750. 46815916
Austria A Wien Wien 83850. 8499759
Czech Republic CZ Praha Praha 78703. 10562214
Germany D Berlin Berlin 356910. 80219695
Hungary H Budapest Budapest 93030. 9937628
Italy I Roma Lazio 301230. 59433744
Liechtenstein FL Vaduz Liechtenstein 160. 36636

Figure 4 – Un extrait de la table country.

• 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

1.2 Requêtes de base


Commençons par extraire de cette table le nom de tous les pays qu’elle contient :

SELECT name FROM c o u n t r y

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 :

SELECT name , c a p i t a l FROM c o u n t r y


SELECT * FROM c o u n t r y

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 :

SELECT c a p i t a l FROM c o u n t r y WHERE name = ' Botswana '

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 :

SELECT name FROM c o u n t r y WHERE c a p i t a l I S NULL

Différentes clauses permettent de formuler des requêtes plus élaborées ; la figure 5 rassemble les instructions
les plus fréquentes.

SELECT * sélection des colonnes


SELECT DISTINCT * sélection sans doublon
FROM table nom d’une table
WHERE condition imposer une condition
GROUP BY expression grouper les résultats
HAVING condition condition sur un groupe
ORDER BY expression ASC / DESC trier les résultats par ordre croissant / décroissant

LIMIT n limiter à n enregistrements (SQLite)


OFFSET n débuter à partir de n enregistrements (SQLite)

OFFSET n ROWS débuter à partir de n enregistrements (Oracle)


FETCH FIRST n ROWS ONLY limiter à n enregistrements (Oracle)

UNION | INTERSECT | EXCEPT opérations ensemblistes sur les requêtes

Figure 5 – Principales requêtes SQL.


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 :

country c JOIN encompasses e ON c.code = e.country

qui possède huit attributs :


c.Code = e.Country c.Name c. Capital c.Province c.Area c.Population e.Continent e.Percentage

NAME CODE CAPITAL PROVINCE AREA POPULATION COUNTRY CONTINENT PERCENTAGE


Bulgaria BG Sofia Bulgaria 110910. 7284552 BG Europe 100
Romania RO Bucuresti Bucuresti 237500. 20121641 RO Europe 100
Turkey TR Ankara Ankara 780580. 75627384 TR Asia 97
Turkey TR Ankara Ankara 780580. 75627384 TR Europe 3
Denmark DK Kobenhavn Hovedstaden 43070. 5580516 DK Europe 100

Figure 6 – Un extrait de la jointure entre les tables country et encompasses.


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 :

Name Country Province Population Longitude Latitude

(l’attribut Country est le code du pays).


Déterminer les capitales européennes situées à une latitude supérieure à 60◦ .

http://info-llg.fr
12.6 informatique commune

1.4 Fonctions d’agrégation


Il est possible de regrouper certains enregistrements d’une table par agrégation à l’aide du mot-clé GROUP BY
pour ensuite leur appliquer une fonction (on trouvera figure 7 quelques exemples de fonctions statistiques
disponibles). Par exemple, pour connaître le nombre de pays de chaque continent on écrira :
SELECT e . c o n t i n e n t , COUNT ( * )
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

COUNT( ) nombre d’enregistrements


MAX( ) valeur maximale d’un attribut
MIN( ) valeur minimale d’un attribut
SUM( ) somme d’un attribut
AVG( ) moyenne d’un attribut

Figure 7 – Fonctions statistiques.

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.

2.1 Opérations ensemblistes


Trois opérations ensemblistes de base peuvent être effectuées avec les relations : les opérations d’union (∪),
d’intersection (∩) et de différence (−).

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

Figure 8 – Les trois opérations ensemblistes de base en SQL.

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

2.2 Opérations spécifiques à l’algèbre relationnelle


Projection
La projection extrait une relation d’une relation donnée en supprimant des attributs de cette dernière. Si
A1 , A2 , . . . , An sont des attributs d’une relation R, la projection de R suivant A1 , A2 , . . . , An est l’ensemble des
enregistrements de R dont les attributs sont A1 , A2 , . . . , An et qui ne se répètent pas. On la note :

π(A1 ,...,An ) (R)

En SQL la projection se traduit par la requête :

SELECT DISTINCT a1, a2, ... , an FROM table

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

Produit et division cartésiens


Le produit cartésien de deux relations R1 et R2 se note :

R1 × R2

et se traduit en SQL par :


SELECT * FROM table1, table2

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

et se caractérise par : ∀x ∈ R1 ÷ R2 , ∀y ∈ R2 , xy ∈ R1 . Cette opération n’a pas d’équivalent en SQL.

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

Les amateurs prendront plaisir à prouver le théorème suivant :

Théorème. — Si S1 = πR1 −R2 (R1 ) et S2 = πR1 −R2 (S1 × R2 − R1 ) alors R1 ÷ R2 = S1 − S2 .



Exercice 5  Donner un sens aux expressions de l’algèbre relationnelle suivantes :
a) πName (σLatitude>66 (city) ∩ σPopulation>10000 (city)) ;
b) πcountry.Name (σcity.Latitude<0 (x) − σcity.Latitude>−23 (x)) avec x = country Zcountry.code=city.country city.


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)).

Est-ce toujours vérifié si A < X ?



Exercice 7  Et maintenant que vous êtes fans d’algèbre relationnelle, prouver le théorème relatif à la division
cartésienne.

http://info-llg.fr
12.10 informatique commune

Annexe : interaction avec une base de données en Python


Le module permettant d’intégrer un SGBD à un environnement Python s’appelle sqlite3 ; une fois ce dernier
importé, on se connecte à une base de données par l’intermédiaire de la fonction connect, en précisant en
paramètre un chemin d’accès vers la base de données. Par exemple, pour utiliser la base de données mondial.sq3
(et en supposant que celle-ci soit dans le répertoire courant) on écrira :
import sqlite3 as sql

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

base = sqlite3.connect ("mondial.sq3")


cur = base.cursor ()

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)

SELECT name FROM c o u n t r y


WHERE p o p u l a t i o n > 60000000

b)

SELECT name FROM c o u n t r y


WHERE p o p u l a t i o n > 60000000
ORDER BY name

c)

SELECT name , p o p u l a t i o n FROM c o u n t r y


WHERE p o p u l a t i o n > 60000000
ORDER BY p o p u l a t i o n DESC

d) Avec SQLite :

SELECT name FROM c o u n t r y


ORDER BY a r e a ASC LIMIT 10

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 :

SELECT name FROM c o u n t r y


ORDER BY a r e a ASC LIMIT 10 OFFSET 10

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)

SELECT DISTINCT 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 . p e r c e n t a g e < 100

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 :

SELECT name , COUNT ( * ) c FROM l a n g u a g e


GROUP BY name
ORDER BY c DESC LIMIT 10

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)

SELECT name FROM l a n g u a g e


GROUP BY name
HAVING COUNT ( * ) = 6

SELECT c . name AS pays , l . name AS l a n g u e


FROM c o u n t r y c JOIN l a n g u a g e l ON c . code = l . c o u n t r y
WHERE l . name IN
( SELECT name FROM l a n g u a g e
GROUP BY name
HAVING COUNT ( * ) = 6 )

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 :

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
LIMIT 5

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 :

(Ai1 , . . . , Aik−1 , a, Aik+1 , . . . , Aip ).

σ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

Vous aimerez peut-être aussi