Exercices corrigés : représentation des données, réels et texte (NSI Première)
Voici quinze exercices corrigés de NSI sur la représentation des nombres réels et du texte, au niveau de la classe de Première du programme français, tel qu'il est suivi au Lycée Marie de France et au Collège Stanislas à Montréal. Ils couvrent l'écriture binaire d'un nombre à virgule, la virgule fixe, les nombres flottants et la norme IEEE 754, les erreurs d'arrondi, l'absorption et la non-associativité de l'addition flottante, le code ASCII, Unicode et l'encodage UTF-8.
À la fin de la série, vous savez convertir un nombre à virgule en binaire et dire pourquoi 0,1 ne s'y écrit pas exactement (exercice 1), coder une valeur en virgule fixe sur un octet (exercice 11), comparer deux flottants à une tolérance près au lieu de tester leur égalité (exercices 3 et 9), passer d'un caractère à son code avec ord et chr (exercices 4 et 13), et calculer la taille en octets d'un texte encodé en UTF-8 (exercice 5).
Le fil de la série : une machine ne manipule que des suites finies de bits, alors que les réels et les alphabets du monde sont infinis. Toute représentation est donc un COMPROMIS, et chaque bug de ce chapitre vient d'un programmeur qui a oublié lequel. Deux pièges reviennent d'un exercice à l'autre : on ne teste jamais l'égalité de deux flottants ni ne pilote une boucle avec, d'où la boucle qui ne s'arrête jamais de l'exercice 6, et le nombre de caractères d'une chaîne n'est pas sa taille en octets, d'où le « résumé » illisible de l'exercice 7.
Ce que le programme exige, et ce qu'il n'exige pas. Il demande de calculer l'écriture binaire de quelques réels, de savoir que 0,1+0,2 n'est pas égal à 0,3 et de ne jamais tester l'égalité de deux flottants, d'identifier l'intérêt des encodages ASCII, ISO-8859-1 et Unicode ; il précise qu'aucune connaissance précise de la norme IEEE-754 ni des normes d'encodage n'est exigible. Le codage bit à bit d'un flottant et d'un caractère UTF-8, exercices 12 et 14, est donc étiqueté comme approfondissement, les règles étant chaque fois rappelées dans l'énoncé.
Série autocorrigéeTape tes réponses et vérifie-les question par question : la page te dit juste ou faux avant d'ouvrir la correction. Avec un compte, chaque bonne réponse du premier coup rapporte des points, et tes réponses justes restent remplies quand tu reviens.
•En binaire, les positions à droite de la virgule pèsent 21, 41, 81, etc.
•Un nombre décimal s'écrit exactement en binaire si et seulement si son dénominateur réduit est une puissance de 2. Ce n'est pas le cas de 0,1.
•Flottant double précision : 64 bits, soit environ 16 chiffres significatifs. Pour information, non exigible : la norme IEEE 754 les répartit en 1 bit de signe, 11 d'exposant et 52 de mantisse.
•Les flottants ne sont pas répartis uniformément : l'écart entre deux voisins double à chaque puissance de 2.
•0,1+0,2 ne vaut pas 0,3. On ne teste jamais l'égalité de deux flottants, on compare à une tolérance près.
•Absorption : ajouter un nombre trop petit à un grand ne change rien du tout au résultat.
•Une variable de boucle doit être un ENTIER, jamais un flottant.
•ASCII : 7 bits, 128 caractères. A vaut 65, a vaut 97, le chiffre 0 vaut 48, l'espace vaut 32.
•Unicode est un répertoire de points de code ; UTF-8 est un encodage à longueur variable, de 1 à 4 octets, compatible ASCII. Le détail des octets n'est pas exigible.
•Nombre de caractères et nombre d'octets sont deux grandeurs distinctes : elles ne coïncident que pour un texte purement ASCII.
Partie A : Les bases (/50)
Exercice 1 : Écrire un nombre à virgule en binaire
En binaire, les chiffres situés à droite de la virgule ont pour poids 2−1=21, 2−2=41, 2−3=81, et ainsi de suite, exactement comme les dixièmes et les centièmes en décimal.
L'écriture ci-dessous se lit donc 101,1012.
a) Convertissez 101,1012 en décimal, en détaillant la somme des poids.
b) Convertissez 0,625 en binaire par la méthode des multiplications successives par 2.
c) Appliquez la même méthode à 0,1. Que constatez-vous au bout de quelques étapes ?
d) Quels sont les nombres décimaux qui s'écrivent EXACTEMENT en binaire avec un nombre fini de chiffres ? Donnez le critère.
e) Le nombre 31 n'a pas d'écriture décimale finie. Est-ce un défaut du système décimal ou une propriété du nombre ? Reliez au cas de 0,1 en binaire.
Voir la correction
Réponses
a)101,1012=5,625
b)0,625=0,1012
c)0,1=0,000112, infini
d)Dénominateur puissance de 2
e)Propriété du couple nombre et base
a)
101,1012=4+0+1+21+0+81=5+0,5+0,125=5,625. On additionne simplement les poids des positions où le bit vaut 1.
b)
0,625×2=1,25, on note 1 et il reste 0,25. 0,25×2=0,5, on note 0 et il reste 0,5. 0,5×2=1,0, on note 1 et il reste 0. Le processus s'arrête : 0,625=0,1012.
c)
0,1×2=0,2, on note 0. 0,2×2=0,4, on note 0. 0,4×2=0,8, on note 0. 0,8×2=1,6, on note 1, reste 0,6. 0,6×2=1,2, on note 1, reste 0,2. On retrouve 0,2, déjà rencontré : le processus BOUCLE. L'écriture est 0,000112, périodique et infinie.
d)
Ce sont exactement les nombres dont le dénominateur, une fois la fraction réduite, est une PUISSANCE DE 2. C'est le cas de 0,625=85, de 0,5 ou de 0,75 ; ce n'est pas le cas de 0,1=101, dont le dénominateur contient un facteur 5.
e)
C'est une propriété du COUPLE nombre-base, pas un défaut d'un système. En base 10, une fraction s'écrit exactement si son dénominateur ne contient que des facteurs 2 et 5 ; en base 2, seulement des facteurs 2. Ainsi 31 échoue dans les deux bases, tandis que 101 passe en décimal et échoue en binaire. C'est de là que viennent toutes les surprises numériques des langages de programmation.
Coche ici les exercices faits ou à revoir : un compte gratuit, sans mot de passe, retient tes coches et tes réponses justes d'une visite à l'autre, te dit quel chapitre attaquer ensuite et te permet de demander l'exercice qui te manque. Crée ton espace, un courriel suffit.
Exercice 2 : La représentation en virgule flottante
Un ordinateur ne stocke pas les réels avec un nombre fixe de décimales, mais sous la forme ±m×2e : un signe, une mantisse m et un exposant e. C'est la norme IEEE 754.
En double précision, un nombre occupe 64 bits : 1 pour le signe, 11 pour l'exposant et 52 pour la mantisse.
Sur le statut de cet exercice : le programme de Première précise qu'« aucune connaissance précise de la norme IEEE-754 n'est exigible ». Le découpage en 1, 11 et 52 bits est donc DONNÉ par l'énoncé, et les questions a) et c), qui comptent sur lui, sont un approfondissement. Les questions b), d) et e) portent sur ce qui est exigible : les flottants sont en nombre fini, ils ne sont pas répartis uniformément, et un calcul peut en sortir.
a) Combien de valeurs différentes le champ d'exposant peut-il coder ? Combien la mantisse ?
b) Combien de nombres flottants différents une machine peut-elle représenter au total, à l'ordre de grandeur près ? Comparez à l'infinité des réels.
c) Le format simple précision utilise 32 bits, dont 23 de mantisse. Combien de chiffres décimaux significatifs cela représente-t-il environ, sachant que 223≈8,4×106 ?
d) Pourquoi les flottants ne sont-ils PAS répartis uniformément sur la droite des réels ? Illustrez avec l'intervalle [1;2] comparé à [2;4].
e) Que se passe-t-il quand un calcul produit un nombre plus grand que le plus grand flottant représentable ? Et plus petit que le plus petit non nul ?
Voir la correction
Réponses
a)211=2048 ; 252≈4,5×1015
b)264≈1,8×1019 : fini
c)Environ 7 chiffres significatifs
d)Autant de flottants, écarts doublés
e)inf par le haut, 0 par le bas
a)
L'exposant sur 11 bits code 211=2048 valeurs. La mantisse sur 52 bits en code 252≈4,5×1015.
b)
Le total est de l'ordre de 264≈1,8×1019 valeurs, un peu moins en réalité car certaines combinaisons sont réservées. C'est un nombre FINI, alors que les réels sont en quantité infinie et même non dénombrable : la quasi-totalité des réels n'est donc pas représentable, et tout calcul flottant est un calcul approché.
c)
La mantisse distingue environ 8,4×106 valeurs par octave, ce qui correspond à un peu moins de 7 chiffres décimaux, puisque 107=10000000 est du même ordre. On retient donc environ 7 chiffres significatifs en simple précision, et environ 16 en double précision.
d)
Parce que l'écriture est m×2e : à mantisse fixée, changer d'exposant multiplie l'ÉCART entre deux flottants voisins. Sur [1;2], l'exposant vaut 0 et il y a 252 valeurs ; sur [2;4], deux fois plus long, l'exposant vaut 1 et il y a exactement le MÊME nombre de valeurs, donc des écarts deux fois plus grands. Les flottants sont serrés près de zéro et de plus en plus espacés en s'éloignant.
e)
Trop grand : c'est le DÉPASSEMENT par le haut, et le résultat devient inf, l'infini flottant, qui contamine ensuite tous les calculs. Trop petit : c'est le dépassement par le bas, et le résultat est arrondi à 0, ce qui est plus insidieux car aucune alerte n'est levée et une division ultérieure produira une erreur inexpliquée.
Exercice 3 : Les pièges des flottants
Puisque 0,1 n'est pas représentable exactement en binaire, la machine stocke la valeur représentable la plus proche. Les erreurs qui en résultent sont minuscules, mais elles se propagent.
En Python, 0,1+0,2 affiche 0,30000000000000004. Le schéma ci-dessous situe 0,1 entre ses deux flottants voisins.
a) Expliquez d'où vient ce chiffre 4 à la dix-septième décimale.
b) Que renvoie l'expression 0,1+0,2==0,3 ? Pourquoi ne faut-il jamais tester l'égalité de deux flottants ?
c) Écrivez le test correct à utiliser à la place, avec une tolérance absolue de 10−9.
d) On ajoute 1,0 et 10−20. Que vaut le résultat en double précision ? Comment s'appelle ce phénomène ?
e) On additionne un million de fois le nombre 0,1. Le résultat vaut-il exactement 100000 ? D'où vient l'écart ?
Voir la correction
Réponses
a)Arrondis de 0,1 et 0,2 cumulés
b)False
c)abs(a - b) < 1e-9
d)1,0 : absorption
e)Non : les arrondis des additions, de signes variés, laissent un excès d'environ 1,3×10−6
a)
Ni 0,1 ni 0,2 ne sont représentables exactement : la machine utilise pour chacun le flottant le plus proche, très légèrement différent. La somme de ces deux approximations n'est pas non plus représentable, et le flottant le plus proche de cette somme n'est pas celui qui représente 0,3. L'écart apparaît à la limite des 16 chiffres significatifs de la double précision, c'est-à-dire à la dix-septième décimale.
b)
Elle renvoie False. Tester l'égalité de deux flottants revient à exiger que deux chaînes d'arrondis distinctes aient produit exactement le même motif de 64 bits, ce qui n'a aucune raison d'arriver. Le test réussit parfois, échoue parfois, et c'est justement ce caractère imprévisible qui en fait un bug redoutable.
c)
On écrit abs(a−b)<10−9 plutôt que a==b. En pratique on préfère une tolérance RELATIVE, abs(a−b)≤tol×max(abs(a),abs(b)), car une tolérance absolue de 10−9 n'a aucun sens pour des nombres de l'ordre de 1015.
d)
Le résultat vaut exactement 1,0. L'écart relatif entre les deux nombres, 10−20, est très inférieur à la précision relative de la double précision, environ 10−16 : le petit nombre disparaît entièrement dans l'arrondi. Ce phénomène s'appelle l'ABSORPTION.
e)
Non : la boucle donne environ 100000,0000013, un excès d'environ 1,3×10−6. L'approximation de 0,1 n'y est presque pour rien : elle dépasse 0,1 d'environ 5,6×10−18, ce qui, multiplié par un million, ne fait que 5,6×10−12. L'écart vient des arrondis des ADDITIONS : chaque somme partielle doit être ramenée au flottant le plus proche, avec une erreur d'autant plus grande que la somme est grande. Ces arrondis changent de signe, par excès à la troisième addition, par défaut aux huit suivantes, puis tantôt dans un sens, tantôt dans l'autre, et ils ne se compensent pas : leur bilan l'emporte de très loin sur l'erreur de départ. L'écart final se lit sur les derniers chiffres significatifs, et c'est exactement pour cela qu'un logiciel de comptabilité ne stocke jamais des euros en flottants, mais des centimes en entiers.
Exercice 4 : Le code ASCII
Le code ASCII associe à chaque caractère un nombre entier compris entre 0 et 127, codé sur 7 bits. Il couvre les lettres non accentuées, les chiffres, la ponctuation et quelques caractères de commande.
Repères à connaître : le caractère A vaut 65, le caractère a vaut 97, le chiffre 0 vaut 48 et l'espace vaut 32. La droite ci-dessous place ces repères parmi les 128 codes.
a) Donnez le code ASCII des caractères C, c et 5, en justifiant chaque calcul.
b) Quelle opération arithmétique transforme une majuscule en la minuscule correspondante ? Justifiez avec les repères donnés.
c) Écrivez 65 en binaire sur 8 bits, puis en hexadécimal.
d) Combien de caractères le code ASCII peut-il représenter au total ? Pourquoi cela s'est-il révélé insuffisant hors du monde anglophone ?
e) Un fichier contient le mot NSI suivi d'un retour à la ligne, dont le code vaut 10. Donnez la suite des quatre codes, puis la taille du fichier en octets.
Voir la correction
Réponses
a)67, 99, 53
b)Ajouter 32=25
c)010000012=0x41
d)128 caractères
e)78, 83, 73, 10 : 4 octets
a)
Les lettres se suivent dans l'ordre alphabétique : C est la troisième lettre, donc 65+2=67. De même c vaut 97+2=99. Le chiffre 5 vaut 48+5=53. On ne mémorise que les points de départ, jamais la table entière.
b)
Il suffit d'AJOUTER 32, puisque 97−65=32. L'écart est le même pour toutes les lettres, ce qui n'est pas un hasard : 32=25, si bien que passer d'une casse à l'autre revient à basculer un seul bit, le sixième.
c)
65=64+1=26+20, donc 65=010000012. En hexadécimal, on regroupe par quartets : 0100 vaut 4 et 0001 vaut 1, d'où 65=4116, noté 0x41.
d)
Sur 7 bits, 27=128 caractères. C'est insuffisant dès qu'on quitte l'anglais : il n'y a de place ni pour les lettres accentuées du français, ni pour le β allemand, ni pour les alphabets cyrillique, grec, arabe, encore moins pour les milliers d'idéogrammes chinois.
e)
N vaut 65+13=78, S vaut 65+18=83, I vaut 65+8=73, et le retour à la ligne vaut 10. La suite est donc 78, 83, 73, 10. Chaque code tenant sur un octet, le fichier pèse 4 octets.
Exercice 5 : Unicode et UTF-8
Unicode attribue à chaque caractère de toutes les écritures du monde un numéro unique, le POINT DE CODE, noté U+ suivi de son écriture hexadécimale. Unicode ne dit pas comment ranger ces numéros en mémoire.
UTF-8 est l'encodage qui répond à cette seconde question : il utilise de 1 à 4 octets par caractère, et les 128 premiers points de code y sont codés exactement comme en ASCII.
Approfondissement pour la question c) : la structure des octets UTF-8, avec leurs bits de tête 110 et 10, n'est pas exigible, le programme précisant qu'« aucune connaissance précise des normes d'encodage n'est exigible ». Les autres questions portent sur ce qui l'est : l'intérêt d'un encodage compatible avec ASCII, et la différence entre nombre de caractères et nombre d'octets.
a) Le caractère A a pour point de code U+0041. Combien d'octets occupe-t-il en UTF-8 ? Justifiez.
b) Le caractère e accentué a pour point de code U+00E9, soit 233 en décimal. Peut-il tenir sur un octet en UTF-8 ? Combien lui en faut-il ?
c) À quoi servent les bits de tête 110 et 10 visibles sur la figure ? Quel avantage cela donne-t-il à un programme qui lit un fichier ?
d) Un document contient 4000 caractères, dont 3600 non accentués et 400 accentués, plus un caractère par ligne de retour à la ligne pour 80 lignes. Calculez sa taille en octets.
e) Distinguez précisément, en une phrase chacun, le nombre de CARACTÈRES et le nombre d'OCTETS d'une chaîne. Lequel des deux la fonction len renvoie-t-elle en Python 3 ?
Voir la correction
Réponses
a)A : 1 octet
b)é : 2 octets, C3 A9
c)110 et 10 : auto-synchronisation
d)4480 octets
e)len compte les caractères
a)
Un seul octet. Son point de code, 0x41=65, est inférieur à 128 : il appartient à la plage compatible ASCII, codée sur un octet dont le premier bit vaut 0. C'est la propriété de compatibilité ascendante qui a fait le succès d'UTF-8, tout fichier ASCII est déjà un fichier UTF-8 valide.
b)
Non, il ne peut pas tenir sur un octet UTF-8 : 233 dépasse 127, et un octet dont le premier bit vaut 1 est réservé aux séquences multi-octets. Il lui faut DEUX octets, 0xC3 puis 0xA9.
c)
Ils indiquent la STRUCTURE de la séquence : un octet commençant par 110 annonce un caractère sur deux octets, et tout octet commençant par 10 est un octet de continuation. Un programme peut donc, en lisant un octet quelconque au milieu d'un fichier, savoir immédiatement s'il est au début d'un caractère ou au milieu, et se resynchroniser sans revenir au début. C'est ce qu'on appelle l'auto-synchronisation.
d)
Les 3600 caractères non accentués font 3600 octets. Les 400 accentués font 400×2=800 octets. Les 80 retours à la ligne font 80 octets. Total : 3600+800+80=4480 octets, soit environ 4,4kio, pour 4080 caractères.
e)
Le nombre de caractères est le nombre de symboles perçus par un lecteur humain ; le nombre d'octets est la place réellement occupée en mémoire, qui dépend de l'encodage choisi. En Python 3, len appliquée à une chaîne renvoie le nombre de CARACTÈRES ; pour obtenir les octets, il faut d'abord encoder la chaîne, par exemple avec la méthode d'encodage en UTF-8.
Partie B : Problèmes et raisonnement (/50)
Exercice 6 : Problème : la boucle qui ne s'arrête jamais
Le programme suivant devrait s'arrêter lorsque x atteint 1,0. Il ne s'arrête pas, et la garde placée sur n le montre.
python
x = 0.0
n = 0
while x != 1.0:
x = x + 0.1
n = n + 1
if n > 20:
print("la boucle ne s'arrete pas, x vaut", x)
break
a) Expliquez précisément pourquoi la condition d'arrêt n'est jamais vérifiée.
b) Combien de tours la boucle fait-elle avant que la garde ne s'active ? Que vaut alors x, à l'affichage près ?
c) Réécrivez la boucle avec une condition d'inégalité. Le problème est-il totalement résolu ?
d) Réécrivez-la avec un compteur ENTIER, en calculant x à partir de ce compteur. Pourquoi cette version est-elle la bonne ?
e) Généralisez : donnez la règle à retenir sur l'emploi des flottants comme variable de boucle.
Voir la correction
Réponses
a)x ne vaut jamais 1.0
b)21 tours, x vaut environ 2,1
c)x < 1.0 : dix ou onze tours
d)Compteur entier, x = k / 10
e)Jamais de flottant pour piloter
a)
Parce que 0,1 n'est pas représentable exactement : la variable x ne prend jamais la valeur exacte 1,0. Après dix additions elle vaut 0,9999999999999999, un flottant très proche de 1 mais différent de lui. Le test d'égalité échoue donc, et la boucle continue indéfiniment.
b)
La garde s'active quand n dépasse 20, donc au vingt et unième tour. La variable x vaut alors environ 2,1, avec les dernières décimales polluées par les arrondis cumulés : le programme est déjà passé deux fois au-delà de la cible sans jamais l'atteindre.
c)
On écrit whilex<1,0. La boucle s'arrête bien, mais le problème n'est pas totalement résolu : selon le sens des arrondis, elle peut faire dix ou onze tours, et l'on ne sait pas lequel sans exécuter le programme. Le comportement reste dépendant de la représentation machine.
d)
On écrit une boucle sur un entier, par exemple pour k allant de 0 à 9, et l'on pose x=k/10 à l'intérieur. Cette version est la bonne parce que le compteur est un ENTIER, représenté exactement : le nombre de tours est garanti, connu à la lecture, et les erreurs d'arrondi n'affectent plus que la valeur de x, jamais le déroulement du programme.
e)
La règle est simple : on ne pilote JAMAIS une boucle avec un flottant. La variable de contrôle doit être un entier, et les valeurs réelles se calculent à partir de cet entier. Corollaire, on ne teste jamais l'égalité de deux flottants, on compare toujours à une tolérance près.
Rappel : en UTF-8, le caractère e accentué s'encode sur deux octets, 0xC3 et 0xA9. En Latin-1, chaque octet est interprété comme un caractère unique, et 0xC3 y désigne le A tilde majuscule tandis que 0xA9 désigne le symbole du copyright. Le schéma ci-dessous représente la lecture du fichier.
a) Expliquez, octet par octet, pourquoi un seul caractère accentué en produit deux à l'affichage.
b) Le mot « résumé » compte 6 caractères. Combien d'octets occupe-t-il en UTF-8 ? Combien de caractères le lecteur Latin-1 croit-il lire ?
c) Le fichier fait 4480 octets. Combien de caractères le lecteur Latin-1 annonce-t-il ? Comparez aux 4080 caractères réels.
d) L'inverse se produit aussi : un fichier Latin-1 lu comme de l'UTF-8. Que se passe-t-il alors, et pourquoi l'erreur est-elle ici plus visible ?
e) Quelles deux précautions un programme doit-il prendre pour éviter durablement ce type de bug ?
Voir la correction
Réponses
a)Deux octets, deux caractères Latin-1
b)8 octets, 8 caractères lus
c)4480 au lieu de 4080
d)Décodage UTF-8 en erreur
e)Déclarer l'encodage, UTF-8 partout
a)
Le caractère accentué est stocké sur DEUX octets, 0xC3 et 0xA9. Le lecteur Latin-1 ignore la notion de séquence multi-octets : il traite chaque octet isolément et le convertit en un caractère. Il affiche donc le caractère de 0xC3, puis celui de 0xA9, soit deux symboles là où il n'y avait qu'une lettre.
b)
Le mot compte 4 caractères non accentués, à un octet chacun, et 2 caractères accentués, à deux octets chacun : 4+4=8 octets. Le lecteur Latin-1 croit lire 8 caractères au lieu de 6.
c)
Le lecteur Latin-1 annonce autant de caractères que d'octets, soit 4480. C'est 400 de plus que les 4080 caractères réels, exactement le nombre de caractères accentués du document, chacun ayant été compté deux fois.
d)
Le décodage UTF-8 ÉCHOUE, car les octets isolés supérieurs à 127 ne forment pas des séquences valides : le programme lève une erreur au lieu d'afficher n'importe quoi. L'erreur est donc plus visible, et paradoxalement plus facile à corriger : un plantage franc vaut mieux qu'un texte silencieusement abîmé, que l'on risque de réenregistrer tel quel et de perdre définitivement.
e)
Premièrement, TOUJOURS spécifier l'encodage explicitement à l'ouverture d'un fichier, plutôt que de se fier à la valeur par défaut du système, qui diffère d'une machine à l'autre. Deuxièmement, choisir UTF-8 partout, à l'écriture comme à la lecture, et le déclarer dans les en-têtes des formats qui le permettent, comme les pages web.
python
mot = "résumé"
print(len(mot)) # 6 : nombre de CARACTERES
print(len(mot.encode("utf-8"))) # 8 : nombre d'OCTETS
# ouvrir un fichier en declarant TOUJOURS l'encodage
with open("notes.txt", "r", encoding="utf-8") as f:
contenu = f.read()
Exercice 8 : Cinq affirmations à corriger
Chacune des cinq affirmations suivantes est FAUSSE. Dites pourquoi et donnez l'énoncé correct.
1) « Un ordinateur en double précision est exact jusqu'à la seizième décimale, donc 0,1+0,2 vaut exactement 0,3. »
2) « Unicode est un encodage qui range les caractères en mémoire. »
3) « En UTF-8, chaque caractère occupe deux octets. »
4) « Les flottants sont répartis régulièrement sur la droite des réels. »
5) « En Python 3, la longueur d'une chaîne est égale à la taille en octets du fichier qui la contient. »
Voir la correction
Réponses
1)Pas de représentation exacte
2)Unicode : répertoire
3)UTF-8 : 1 à 4 octets
4)Écarts croissants
5)Caractères contre octets
1)
FAUX. La double précision garantit environ 16 chiffres SIGNIFICATIFS, ce qui n'est pas la même chose qu'un nombre de décimales exactes, et surtout 0,1 et 0,2 ne sont pas représentables exactement en binaire. Leur somme vaut 0,30000000000000004 et diffère de 0,3.
2)
FAUX. Unicode est un RÉPERTOIRE : il attribue un numéro, le point de code, à chaque caractère, sans dire comment le stocker. C'est l'encodage, UTF-8, UTF-16 ou UTF-32, qui décide de la mise en octets.
3)
FAUX. UTF-8 est un codage à longueur VARIABLE, de 1 à 4 octets selon le caractère. Les 128 premiers points de code, ceux d'ASCII, tiennent sur un seul octet, les lettres accentuées sur deux, la plupart des idéogrammes sur trois.
4)
FAUX. L'écart entre deux flottants consécutifs DOUBLE à chaque puissance de deux : ils sont très serrés près de zéro et de plus en plus espacés vers les grandes valeurs. C'est la conséquence directe de l'écriture m×2e.
5)
FAUX. La fonction de longueur renvoie un nombre de CARACTÈRES, alors que la taille du fichier se compte en OCTETS. Les deux ne coïncident que si tous les caractères sont codés sur un octet, c'est-à-dire pour un texte purement ASCII, sans le moindre accent.
Exercice 9 : Problème : comparer correctement deux flottants
Puisque l'égalité stricte est inutilisable, on écrit une fonction de comparaison approchée. Le code ci-dessous en propose une, à tolérance relative.
python
def presque_egaux(a, b, tol=1e-9):
"""Vrai si a et b sont egaux a la tolerance relative pres."""
if a == b: # cas exact, et cas des zeros
return True
echelle = max(abs(a), abs(b))
return abs(a - b) <= tol * echelle
print(0.1 + 0.2 == 0.3) # False
print(presque_egaux(0.1 + 0.2, 0.3)) # True
a) Expliquez le rôle de la première ligne du corps de la fonction, celle qui teste l'égalité stricte.
b) Pourquoi utiliser une tolérance RELATIVE plutôt qu'absolue ? Donnez un cas où une tolérance absolue de 10−9 est absurde.
c) Testez mentalement la fonction sur a=0 et b=10−20. Que renvoie-t-elle ? Est-ce le comportement souhaité ?
d) Proposez une amélioration qui règle ce cas, en combinant une tolérance absolue et une tolérance relative.
e) Cette fonction est-elle transitive, c'est-à-dire vaut-il que si a est proche de b et b proche de c, alors a est proche de c ? Justifiez et concluez sur l'usage prudent de ce genre de test.
Voir la correction
Réponses
a)Égalité stricte : cas des zéros
b)Précision relative des flottants
c)False pour 0 et 10−20
d)Plancher absolu et tolérance relative
e)Non transitive
a)
Elle traite les cas où les deux valeurs sont rigoureusement identiques, ce qui arrive souvent en pratique. Surtout, elle règle le cas particulier de deux zéros : l'échelle valant alors 0, la tolérance relative serait nulle et le test suivant échouerait à reconnaître que 0 est égal à 0.
b)
Parce que la précision d'un flottant est elle-même RELATIVE : environ 16 chiffres significatifs, quel que soit l'ordre de grandeur. Une tolérance absolue de 10−9 est absurde pour deux nombres de l'ordre de 1015, où l'écart entre deux flottants consécutifs dépasse déjà 1 : le test échouerait toujours, même pour des valeurs voisines. Elle est tout aussi absurde pour des nombres de l'ordre de 10−20, où elle déclarerait égaux des nombres différant d'un facteur mille.
c)
L'échelle vaut max(0,10−20)=10−20, et la tolérance 10−9×10−20=10−29. L'écart 10−20 lui est très supérieur, donc la fonction renvoie faux. Ce n'est pas le comportement souhaité : à l'échelle d'un calcul ordinaire, 10−20 est indiscernable de zéro, et il faudrait les déclarer égaux.
d)
On ajoute un plancher absolu : renvoyer vrai si abs(a−b)≤max(tolabs,tolrel×max(abs(a),abs(b))), avec par exemple tolabs=10−12. Le plancher prend le relais près de zéro, la tolérance relative gouverne partout ailleurs. C'est exactement la stratégie de la fonction de comparaison approchée de la bibliothèque standard.
e)
Non, elle n'est PAS transitive. Avec une tolérance de 0,1 en absolu, 1,00 est proche de 1,09 et 1,09 est proche de 1,18, alors que 1,00 et 1,18 diffèrent de 0,18. La proximité approchée n'est donc pas une relation d'équivalence, et l'on ne peut pas s'en servir pour regrouper des valeurs ni pour trier. Elle sert à valider un résultat de calcul, rien de plus.
python
def presque_egaux(a, b, tol_rel=1e-9, tol_abs=1e-12):
"""Vrai si a et b sont egaux a une tolerance pres :
absolue pres de zero, relative partout ailleurs."""
return abs(a - b) <= max(tol_abs, tol_rel * max(abs(a), abs(b)))
print(presque_egaux(0.1 + 0.2, 0.3)) # True
print(presque_egaux(0.0, 1e-20)) # True : le plancher absolu joue
print(presque_egaux(1e15, 1e15 + 1)) # True : ecart relatif 1e-15
Exercice 10 : Problème : une histoire des représentations
Les choix de représentation ne sont pas tombés du ciel : chacun répond à un problème de son époque. La frise ci-dessous en donne les jalons principaux.
a) Qu'apporte l'architecture de von Neumann, en 1945, quant à la représentation des données ?
b) Le code ASCII est normalisé en 1963 sur 7 bits. Pourquoi ce choix, alors que les machines travaillaient déjà par octets de 8 bits ?
c) La norme IEEE 754 date de 1985. Que se passait-il avant, et pourquoi une norme était-elle devenue indispensable ?
d) Unicode paraît en 1991 et UTF-8 en 1993. Pourquoi deux dates, et pourquoi UTF-8 s'est-il imposé face à UTF-32, qui code tout caractère sur quatre octets ?
e) En une phrase de synthèse, dites ce que ces quatre normes ont en commun quant au rôle d'une norme en informatique.
Voir la correction
Réponses
a)Données et instructions en mémoire
b)Huitième bit : parité
c)Avant 1985 : un format par constructeur
d)UTF-8 : compatible et économique
e)Une norme permet l'échange
a)
Elle pose que les INSTRUCTIONS et les DONNÉES sont stockées dans la même mémoire, sous la même forme binaire. Une même suite de bits peut donc être lue comme un nombre ou comme une instruction selon le contexte : c'est ce qui rend un ordinateur programmable, et c'est aussi ce qui rend indispensable de savoir toujours quel TYPE on manipule.
b)
Parce que le huitième bit servait au CONTRÔLE DE PARITÉ, un mécanisme de détection d'erreur sur les liaisons de l'époque, peu fiables. Les 128 codes de l'ASCII suffisaient à l'anglais, et le bit restant valait mieux comme sécurité de transmission. C'est ce huitième bit libéré qui accueillera plus tard les extensions nationales, dont Latin-1, avec les incompatibilités que l'on sait.
c)
Avant 1985, chaque constructeur avait son propre format de flottants, avec des tailles, des arrondis et des comportements différents devant le zéro et l'infini. Un même programme scientifique donnait des résultats différents d'une machine à l'autre, et parfois faux. La norme est devenue indispensable dès que les programmes ont circulé et que les résultats de calcul ont dû être reproductibles.
d)
Deux dates parce que ce sont deux choses différentes : Unicode définit le répertoire de caractères, UTF-8 définit une façon de le mettre en octets. UTF-8 s'est imposé pour deux raisons décisives : il est compatible avec ASCII, donc les milliards de fichiers et de protocoles existants sont restés valides sans modification, et il est économique, un texte occidental n'occupant guère plus de place qu'en ASCII, là où UTF-32 le quadruplerait.
e)
Toutes quatre montrent qu'une norme, en informatique, ne sert pas à imposer la meilleure solution technique dans l'absolu, mais à permettre à des machines et à des programmes écrits par des gens différents d'échanger sans se tromper sur le sens des bits.
Partie C : les classiques (/50)
Exercice 11 : La virgule fixe sur un octet
Avant les flottants, et encore aujourd'hui dans les microcontrôleurs, on code souvent un nombre positif en VIRGULE FIXE : sur un octet, les 4 bits de gauche donnent la partie entière et les 4 bits de droite la partie fractionnaire, de poids 21, 41, 81 et 161. On note l'octet avec une virgule, par exemple 1011,0110. Le schéma ci-dessous donne le poids de chaque position.
a) Quelle valeur représente l'octet 1011,0110 ?
b) Quelle est la plus grande valeur représentable ? Quel est l'écart entre deux valeurs voisines ?
c) On veut coder 6,8. Donnez l'octet obtenu en tronquant, puis celui obtenu en arrondissant au plus proche, et l'erreur commise dans chaque cas.
d) Montrez qu'additionner deux nombres en virgule fixe revient à additionner les deux octets comme des entiers. Calculez 3,25+5,5 de cette façon.
e) On multiplie 2,5 par 1,5 en multipliant les octets vus comme des entiers, 40 et 24. Par quel nombre faut-il diviser le résultat pour retrouver la bonne valeur ?
Voir la correction
Réponses
a)16182=11,375
b)15,9375 ; pas 0,0625
c)6,75 tronqué, 6,8125 arrondi
d)52+88=140 : 8,75
e)Diviser par 16 après un produit
a)
Partie entière : 10112=8+2+1=11. Partie fractionnaire : 0110 donne 41+81=0,375. L'octet représente 11,375. Autre lecture, très utile : l'octet vu comme un entier vaut 182, et 16182=11,375 ; un nombre en virgule fixe est un entier divisé par 24=16.
b)
La plus grande valeur est 1111,1111, soit 16255=15,9375. Deux valeurs voisines diffèrent de 161=0,0625 : c'est la RÉSOLUTION, la même partout, contrairement aux flottants dont l'écart grandit avec la valeur.
c)
6,8×16=108,8. En tronquant, on garde 108, soit 0110,1100, qui vaut 6,75 : erreur 0,05. En arrondissant, on garde 109, soit 0110,1101, qui vaut 6,8125 : erreur 0,0125. Dans les deux cas 6,8 n'est pas représentable exactement, pour la même raison que 0,1 en flottant : son dénominateur n'est pas une puissance de 2.
d)
Chaque nombre est un entier divisé par 16, donc 16a+16b=16a+b : on additionne les entiers et la virgule reste au même endroit. 3,25 correspond à 52 et 5,5 à 88 ; 52+88=140, soit 16140=8,75, codé 1000,1100. C'est tout l'intérêt de la virgule fixe : un processeur sans calcul flottant sait le faire.
e)
16a×16b=256a×b : le produit des entiers est seize fois trop grand pour être relu en virgule fixe. 40×24=960, et 16960=60, qui se relit 1660=3,75=2,5×1,5. Il faut donc diviser par 16, c'est-à-dire décaler de 4 bits vers la droite, après chaque multiplication.
Exercice 12 : Coder un flottant sur 32 bits
En simple précision, un flottant occupe 32 bits : 1 bit de signe, 8 bits d'exposant et 23 bits de mantisse. Le nombre s'écrit (−1)s×1,m×2E, où m est la suite des bits de mantisse, et l'exposant est stocké avec un DÉCALAGE de 127 : on range E+127.
Approfondissement, au-delà du programme : le programme de Première demande de calculer sur quelques exemples l'écriture binaire d'un réel, comme à la question a), mais précise qu'« aucune connaissance précise de la norme IEEE-754 n'est exigible ». Le codage bit à bit, avec le décalage de 127 et le 1 implicite de la mantisse, des questions b) à e), va au-delà ; toutes les règles nécessaires sont rappelées dans l'énoncé. Le schéma ci-dessous représente les trois champs.
a) Écrivez 6,25 en binaire.
b) Mettez ce nombre sous la forme 1,m×2E. Que vaut E ? Quels sont les premiers bits de la mantisse ?
c) Quel exposant stocké obtient-on ? Écrivez-le sur 8 bits.
d) Assemblez les 32 bits de −6,25 et donnez l'écriture hexadécimale.
e) Décodez le flottant 0x41200000.
Voir la correction
Réponses
a)6,25=110,012
b)1,1001×22
c)129=100000012
d)−6,25 : 0xC0C80000
e)0x41200000=10,0
a)
6=1102 et 0,25=41=0,012, donc 6,25=110,012.
b)
On déplace la virgule de deux rangs vers la gauche : 110,012=1,10012×22. Donc E=2, et la mantisse commence par 1001 ; le 1 placé avant la virgule n'est PAS stocké, puisqu'il vaut toujours 1.
c)
Exposant stocké : 2+127=129=128+1=100000012. Le décalage évite de coder un signe pour l'exposant : les exposants négatifs correspondent aux valeurs stockées inférieures à 127.
d)
Signe 1 pour un nombre négatif, exposant 10000001, mantisse 1001 suivie de 19 zéros. Les 32 bits s'écrivent 1100000011001000…0, regroupés par quatre : 11000000110010000000000000000000, soit 0xC0C80000. Le piège est d'oublier le bit de signe devant l'exposant, ce qui décale tous les regroupements.
e)
0x41200000 s'écrit 0100000100100000… Signe 0 : positif. Exposant 100000102=130, donc E=130−127=3. Mantisse 01 suivie de zéros : 1,012=1,25. Valeur : 1,25×23=10. Le flottant vaut 10,0.
Exercice 13 : ord, chr et les fins de ligne
En Python, ord(c) renvoie le point de code du caractère c, et chr(n) le caractère de point de code n. Un retour à la ligne s'écrit '\n', de code 10 ; un retour chariot s'écrit '\r', de code 13. Les fichiers texte de Linux et de macOS terminent chaque ligne par '\n', ceux de Windows par la paire '\r\n'.
a) Donnez ord('A'), chr(97), ord('é') et chr(8364).
b) Écrivez une fonction majuscule(c) qui transforme une lettre minuscule non accentuée en majuscule avec ord et chr, sans utiliser upper.
c) Que renvoie chr(ord('Z') + 1) ? Pourquoi un chiffrement par décalage écrit naïvement produit-il des symboles au lieu de lettres ?
d) Un fichier contient trois lignes, abc, de et f, chacune terminée par une fin de ligne, en ASCII. Quelle est sa taille en octets sous Linux ? sous Windows ?
e) Que vaut len('abc\nde\nf\n') ? Un programme compte les lignes d'un fichier Windows en cherchant '\n' : trouve-t-il le bon nombre ? Que risque-t-il en comparant la ligne lue à 'abc' ?
Voir la correction
Réponses
a)65, 'a', 233, '€'
b)chr(ord(c) - 32)
c)'[' : modulo 26 nécessaire
d)9 octets sous Linux, 12 sous Windows
e)9 caractères ; 'abc\r' différent de 'abc'
a)
ord('A') vaut 65 et chr(97) vaut 'a', comme en ASCII. ord('é') vaut 233, soit U+00E9 : ord renvoie le point de code Unicode, pas un octet de l'encodage. chr(8364) vaut '€', de point de code U+20AC.
b)
Les minuscules sont 32 rangs après les majuscules : return chr(ord(c) - 32). On peut aussi écrire chr(ord(c) - ord('a') + ord('A')), qui ne demande de retenir aucun nombre.
c)
ord('Z') vaut 90, donc chr(91) vaut '[' : après Z, la table ne recommence pas à A, elle continue avec des symboles. Un décalage doit donc ramener la position dans l'alphabet par un modulo 26 : chr((ord(c) - 65 + k) % 26 + 65).
d)
Les caractères valent 3 + 2 + 1 = 6 octets. Sous Linux, trois fins de ligne d'un octet : 9 octets. Sous Windows, trois paires '\r\n' de deux octets : 12 octets. Le même texte n'a pas la même taille selon le système, sans qu'aucun caractère visible ne diffère.
e)
len('abc\nde\nf\n') vaut 9 : '\n' est UN caractère, même s'il s'écrit avec deux symboles dans le code source. En cherchant '\n', le programme trouve bien trois lignes, mais la première lue vaut 'abc\r' : elle contient un retour chariot invisible, et la comparaison avec 'abc' échoue. D'où l'habitude de retirer les fins de ligne avec strip, ou d'ouvrir les fichiers en mode texte, qui convertit '\r\n' en '\n'.
Exercice 14 : Problème : encoder à la main en UTF-8
UTF-8 range le point de code d'un caractère dans des « cases » de bits libres, notées x, selon le nombre d'octets nécessaires :
1 octet : 0xxxxxxx. 2 octets : 110xxxxx 10xxxxxx. 3 octets : 1110xxxx 10xxxxxx 10xxxxxx. 4 octets : 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx. On remplit les x avec les bits du point de code, alignés à droite.
Approfondissement, au-delà du programme : le programme de Première précise qu'« aucune connaissance précise des normes d'encodage n'est exigible ». L'encodage et le décodage bit à bit des questions a), b), c) et e) vont donc au-delà, et toutes les règles nécessaires sont rappelées ci-dessus. La question d), qui compte caractères et octets, est au programme. Le schéma ci-dessous représente les quatre formats.
a) Combien de bits libres offre chaque format ? Déduisez-en le plus grand point de code codable sur 1, 2 et 3 octets, en hexadécimal.
b) Le symbole de l'euro a pour point de code U+20AC. Combien d'octets lui faut-il ? Encodez-le et donnez les octets en hexadécimal.
c) Décodez la séquence d'octets E4B8AD et donnez le point de code obtenu.
d) La chaîne « Prix : 20 € » compte combien de caractères ? Combien d'octets en UTF-8 ?
e) Un programme coupe un texte UTF-8 à exactement 12 octets pour l'afficher sur un petit écran. Que se passe-t-il si la coupure tombe au milieu du symbole euro ? Comment un programme peut-il reculer jusqu'au début du caractère ?
Voir la correction
Réponses
a)7, 11, 16 et 21 bits libres
b)€ : E2 82 AC
c)E4 B8 AD : U+4E2D
d)11 caractères, 13 octets
e)Reculer sur les octets 10xxxxxx
a)
Sur 1 octet, 7 bits libres : jusqu'à 27−1=127=0x7F. Sur 2 octets, 5+6=11 bits : jusqu'à 211−1=2047=0x7FF. Sur 3 octets, 4+6+6=16 bits : jusqu'à 0xFFFF. Sur 4 octets, 3+6+6+6=21 bits.
b)
0x20AC dépasse 0x7FF mais pas 0xFFFF : il faut 3 octets. En binaire sur 16 bits : 0010000010101100. On répartit 4, 6 et 6 bits : 0010, puis 000010, puis 101100. Les octets sont 11100010, 10000010 et 10101100, soit E282AC.
c)
E4=11100100 annonce 3 octets et porte 0100. B8=10111000 porte 111000. AD=10101101 porte 101101. En recollant : 0100111000101101=01001110001011012=0x4E2D. C'est le point de code U+4E2D, un idéogramme chinois qui signifie « milieu ».
d)
La chaîne compte 11 caractères : P, r, i, x, espace, deux-points, espace, 2, 0, espace et €. Les dix premiers sont en ASCII, un octet chacun ; l'euro en prend trois. Total : 10+3=13 octets.
e)
Les 12 premiers octets contiennent les 10 caractères ASCII et les 2 premiers octets de l'euro : la séquence est incomplète, et l'affichage montre un symbole de remplacement ou déclenche une erreur de décodage. Pour reculer au début d'un caractère, il suffit de regarder les octets à partir de la coupure : tout octet de la forme 10xxxxxx est une continuation ; on recule tant qu'on en rencontre, puis on coupe avant l'octet de tête trouvé. Ici on coupe à 10 octets.
Exercice 15 : Problème : l'ordre des additions compte
En mathématiques, l'addition est associative : (a+b)+c=a+(b+c). Pour les flottants de la double précision, ce n'est plus vrai. On rappelle que les entiers sont représentés exactement jusqu'à 253≈9,0×1015, et qu'au-delà l'écart entre deux flottants voisins atteint 2, puis 4, et ainsi de suite.
a) Que valent (1e16 + 1.0) - 1e16 et (1e16 - 1e16) + 1.0 en Python ? Expliquez la différence.
b) Quel est l'écart entre deux flottants voisins autour de 1016 ? Et autour de 1, sachant qu'il vaut 2−52 ?
c) On additionne dix fois 0.1 dans une boucle en partant de 0.0. Le résultat est-il égal à 1.0 ?
d) On calcule S=1+21+⋯+1061 de deux façons : en commençant par 1, ou en commençant par 1061. Les deux résultats diffèrent dans les dernières décimales. Lequel est le plus précis, et pourquoi ?
e) La bibliothèque standard propose math.fsum, qui additionne une liste en compensant les arrondis. Que vaut math.fsum([0.1] * 10) == 1.0 ? Quand faut-il plutôt travailler en entiers ?
Voir la correction
Réponses
a)0.0 contre 1.0 : non associative
b)2 près de 1016, 2,2×10−16 près de 1
c)0.9999999999999999
d)Petits termes d'abord
e)fsum exact ici ; entiers pour l'argent
a)
(1e16 + 1.0) - 1e16 vaut 0.0 : 1016+1 n'est pas représentable, car l'écart entre flottants voisins y vaut 2, et il est arrondi à 1016 ; la soustraction donne 0. (1e16 - 1e16) + 1.0 vaut 1.0 : la soustraction est exacte et le 1 est ajouté à 0. Même opérations, ordre différent, résultats différents : l'addition flottante n'est pas associative.
b)
Autour de 1016, au-delà de 253, l'écart vaut 2. Autour de 1, il vaut 2−52≈2,2×10−16. L'écart est proportionnel à l'ordre de grandeur : c'est pour cela qu'on parle d'environ 16 chiffres significatifs, et non de 16 décimales.
c)
Non : la boucle donne 0.9999999999999999, et le test d'égalité avec 1.0 est faux. Les dix petites erreurs d'arrondi se cumulent au lieu de se compenser.
d)
Le résultat obtenu en commençant par les PETITS termes est le plus précis. En partant de 1, la somme atteint vite 10 puis 14, et chaque petit terme k1 est ajouté à un grand nombre : il subit l'arrondi à l'échelle de ce grand nombre, et les erreurs s'accumulent sur un million d'additions. En partant des petits, ils s'additionnent d'abord entre eux, à leur propre échelle, et leur somme n'est absorbée qu'une fois devenue significative. On mesure un écart de l'ordre de 10−13 entre les deux ordres, l'ordre croissant des termes étant environ quinze fois plus proche de la valeur exacte.
e)
math.fsum([0.1] * 10) == 1.0 vaut True : fsum garde la trace des erreurs d'arrondi et les réinjecte. On travaille plutôt en ENTIERS dès que le résultat doit être exact par nature, comme des montants d'argent comptés en centimes ou des durées comptées en millisecondes : un entier ne subit aucun arrondi, et l'égalité redevient un test fiable.
Il manque quelque chose dans cette série ?Un type d'exercice que ton prof donne, un énoncé qui te bloque, une erreur repérée : dis-le-moi, je lis chaque demande.
L'envoi passe par ton espace gratuit : c'est ce qui me permet de te répondre et de te prévenir quand l'exercice est ajouté. Un courriel suffit, pas de mot de passe. Crée ton espace.
Pas envie de créer un compte ? Écris-moi directement.
Vous cherchez un tuteur de NSI en Première à Montréal ?
Contactez-moi pour une première séance. Ce chapitre semble théorique jusqu'au jour où un programme donne un résultat faux sans planter, et c'est alors le seul qui permette de comprendre pourquoi.