Mathématice, intégration des Tice dans l'enseignement des mathématiques  
Sommaire > N°66 - Septembre 2019 - en cours d’élaboration > Python sait-il vraiment calculer ?

Python sait-il vraiment calculer ?
Moteur de recherche
Mis en ligne le 1er juillet 2019, par François Goichot

Cet article peut être librement diffusé et son contenu réutilisé suivant la licence CC-by-sa

Si vous tapez 0.1 + 0.1 + 0.1 - 0.3 dans la console Python, qu’attendez-vous en retour ? Sur un ordinateur standard, la réponse est 5.551115123125783e-17, autrement dit $5,551115123125783\,\times\, 10^{-17}$, ce qui n’est pas bien gros mais pas 0 quand même. Voilà déjà de quoi instiller un doute sur la fiabilité de l’utilité de Python pour les calculs, d’autant que n’importe quelle calculatrice, à la question précédente, donne la bonne réponse - nous y reviendrons.

Mais voici un premier exemple de ce qui peut se produire à cause de ces approximations.
Fixons deux entiers strictement positifs $p$ et $q$, et considérons la suite $(u_n)_n$ définie par son premier terme $u_0$ et la relation de récurrence $u_{n+1} = (q+1)\,u_n - p$. Si $u_0$ vaut $\dfrac{p}{q}$, en réduisant au même dénominateur on obtient immédiatement $u_1 = u_0$ et, en itérant, on voit que la suite est constante. Qu’en est-il avec Python ? Demandons-lui de calculer les 100 premiers termes, et la différence avec la valeur théorique ; nous n’afficherons qu’un terme sur 10, pour mieux voir. Voici le programme [1] :

  1. def suite(p,q):
  2. u = p/q
  3. for i in range(100): # i varie de 0 à 99
  4. u = (q+1) * u - p
  5. if (i+1) % 10 == 0 : # l'indice mathématique est décalé de 1
  6. print('Le terme de rang '+str(i+1)+' vaut : '+str(u))
  7. print(' écart avec la valeur théorique : ',u-p/q)

Télécharger

Et voici son exécution pour $p=2, q=3$ :

>>> suite(2,3)
Le terme de rang 10 vaut : 0.6666666666278616
 écart avec la valeur théorique :  -3.8805070268210784e-11
Le terme de rang 20 vaut : 0.6666259765625
 écart avec la valeur théorique :  -4.069010416662966e-05
Le terme de rang 30 vaut : -42.0
 écart avec la valeur théorique :  -42.666666666666664
Le terme de rang 40 vaut : -44739242.0
 écart avec la valeur théorique :  -44739242.666666664
Le terme de rang 50 vaut : -46912496118442.0
 écart avec la valeur théorique :  -46912496118442.664
Le terme de rang 60 vaut : -4.9191317529892135e+19
 écart avec la valeur théorique :  -4.9191317529892135e+19
Le terme de rang 70 vaut : -5.1580834970224175e+25
 écart avec la valeur théorique :  -5.1580834970224175e+25
Le terme de rang 80 vaut : -5.4086425609737785e+31
 écart avec la valeur théorique :  -5.4086425609737785e+31
Le terme de rang 90 vaut : -5.671372782015641e+37
 écart avec la valeur théorique :  -5.671372782015641e+37
Le terme de rang 100 vaut : -5.946865386274833e+43
 écart avec la valeur théorique :  -5.946865386274833e+43

C’est beaucoup plus gênant que le $10^{-17}$ précédent ! Si on creuse un peu, on voit que le problème n’est pas systématique : pour certaines valeurs de $p$ et $q$, la suite est bien constante. Voici quelques tests, où nous n’affichons que le terme de rang 100 :

>>> suite(1,2)
Le terme de rang 100 vaut : 0.5
 écart avec la valeur théorique :  0.0

Cette fois tout est normal.

>>> suite(2,7)
Le terme de rang 100 vaut : -3.2308060613090455e+73
 écart avec la valeur théorique :  -3.2308060613090455e+73

On retrouve le problème précédent. Serait-ce dû au fait que notre premier terme n’est pas décimal ?

>>> suite(2,5)
Le terme de rang 100 vaut : 3.868423350539693e+61
 écart avec la valeur théorique :  3.868423350539693e+61

Ah ben non, c’est plus compliqué que ça. Il va falloir se plonger dans la représentation des nombres en machine.

Le codage des nombres non entiers

Ce qui suit s’applique à Python sur un ordinateur ; les calculatrices n’ont en général pas le même problème, nous y revenons dans un autre onglet.
Commençons par un exercice qu’on peut donner en première (programme actuel ou futur) à condition sans doute de le décomposer : calculer $S= \sum_{n=1}^{+\infty}\,\dfrac{1}{2^{4n}}$ . En mettant $\dfrac{1}{2}$ en facteur (ou en ajoutant 1 qu’on retranchera ensuite), on fait démarrer la somme à $n=0$ et on peut utiliser la formule bien connue $1+q+q^2+\ldots +q^n = \dfrac{1-q^{n+1}}{1-q}$, ici pour $q=\dfrac{1}{2^4}$ ; puis en passant à la limite, on trouve $S = \dfrac{1}{15}$. Cette fraction peut donc s’écrire en binaire : $S = (0,000100010001001\ldots)_2$ , le bloc 0001 se répétant à l’infini, ce que nous conviendrons de noter $(0,\overline{0001})_2$.

L’écriture binaire pour les nombres fractionnaires

Nous insérons ici quelques explications pour les lecteurs qui auraient un peu oublié les changements de base, ou ne les auraient pratiqués que pour les entiers. De même que $327=3\times 10^2+2\times 10^1+7\times 10^0$, en décomposant en puissances de 2 on peut écrire $327 = 256+64+4+2+1 = 1\times 2^8+0\times 2^7+1\times 2^6+0\times 2^5+0\times 2^4+0\times 2^3+1\times 2^2+1\times 2^1+1\times 2^0$ qu’on abrège en $(101000111)_2$ ; les coefficients dans ce dernier cas ne peuvent être que des 1 ou des 0 (sinon, on verrait apparaître une autre puissance de 2), les chiffres utilisables en base 2 sont donc seulement 0 et 1. C’est le même principe pour les nombres fractionnaires : de même que $0,6875=6\times 10^{-1}+8\times 10^{-2}+7\times 10^{-3}+5\times 10^{-4}$, en décomposant en puissances (négatives) de 2 on peut écrire $0,6875 = 0,5+0,125+0,0625 = 1\times 2^{-1}+0\times 2^{-2}+1\times 2^{-3}+1\times 2^{-4}$ qu’on abrège en $(0,1011)_2$.

En fait, de même qu’on montre (mais c’est un peu long...) que tout nombre réel peut s’écrire en base 10, de manière essentiellement unique [2], on peut montrer que tout réel s’écrit en base 2, de manière essentiellement unique [3] Bien sûr, comme en base 10, on peut avoir une infinité de chiffres non nuls après la virgule.

Pour $S’= \sum_{n=1}^{+\infty}\,\dfrac{1}{2^{4n+1}}$ on peut utiliser un calcul analogue au précédent, ou plus économiquement remarquer que $S’$ est la moitié de $S$, donc $\dfrac{1}{30}$. Or en binaire, diviser par 2 revient à décaler la virgule vers la gauche, donc $S’$ s’écrit $(0,0000100010001001\ldots)_2$ c’est -à-dire $(0,0\overline{0001})_2$.

Ainsi $\dfrac{1}{10} = \dfrac{3}{30} = \dfrac{2}{30} + \dfrac{1}{30} = \dfrac{1}{15} + \dfrac{1}{30}$ s’écrit en binaire $(0,00011001100110011\ldots)_2$ c’est-à-dire $(0,0\overline{0011})_2$.

Voyons maintenant comment un ordinateur peut s’en accommoder. Ne pouvant stocker une infinité de chiffres, il doit les tronquer. Pour des raisons évidentes de compatibilité matérielle et logicielle, la façon de tronquer est standardisée, régie par une norme internationale, IEEE 754, bien décrite sur Wikipédia. Regardons ce que donne cette norme sur notre $0,1 = (0,00011001100110011\ldots)_2$ et un ordinateur qui travaille avec des « mots » de 64 bits, comme presque tous les ordinateurs personnels actuels. La virgule (flottante, c’est bien pour cela que les nombres - non entiers - des ordinateurs sont appelés « flottants ») est placée après le premier chiffre non nul, donc après le 1 qui est en quatrième position (quatrième après la « vraie » virgule, celle de la représentation ci-dessus). Ce chiffre 1-là n’est pas enregistré dans l’ordinateur : un chiffre binaire non nul, c’est forcément 1, donc inutile de perdre de la place pour ça. Les 52 chiffres suivants sont enregistrés, cela forme la « mantisse » de notre nombre ; tous les suivants sont ignorés. L’exposant, ici -4, est codé sur les 11 bits restants, sous une forme un peu compliquée qui n’a pas d’importance ici. En résumé, la décomposition binaire ci-dessus de 0,1 est tronquée juste après le 56-ième chiffre.

Vérifions-le... avec Python, mais prudemment cette fois !

  1. def undixieme(N):
  2. '''retourne (0,00011001100110011...)_2 tronqué au N-ième chiffre
  3. après la virgule'''
  4. puiss = 1/2
  5. i = 1
  6. S = 0
  7. while i < N :
  8. puiss = puiss / 2
  9. i += 1
  10. if i % 4 == 0 or i % 4 == 1 : # le chiffre de rang i est un 1
  11. S += puiss
  12. return S

Télécharger

Testons :

>>> 0.1 - undixieme(56)
1.3877787807814457e-17

>>> 0.1 - undixieme(57)
0.0

Ainsi, Python remplace tous les nombres (non entiers) par des valeurs approchées, sauf ceux qui ont en binaire un développement fini : à partir d’un certain rang il n’y a plus que des 0. Ces nombres sont appelés « dyadiques », ce sont les équivalents des décimaux quand on remplace la base 10 par la base 2. Ce sont les nombres qui peuvent s’écrire comme quotient d’un entier par une puissance de 2. Pour tous les autres, avec Python - comme avec tout système de calcul numérique - il y a approximation, donc risque d’erreur.

Une première conséquence : à partir du moment où on travaille avec des flottants, il faut absolument éviter de tester une égalité. Par exemple [4], selon la réciproque du théorème de Pythagore, le triangle de côtés $\sqrt{3}, \sqrt{5}$ et $2\sqrt{2}$ est rectangle. Mais si l’on teste :

>>> from math import sqrt
>>> a, b, c = sqrt(3), sqrt(5), 2*sqrt(2)
>>> a**2 +  b**2 == c**2
False

Il faut donc remplacer un tel test par une estimation $|a^2+b^2-c^2| < \varepsilon$ pour un $\varepsilon > 0$ très petit.

Si on pouvait rester entier...

Il y a un domaine - parmi bien d’autres - où Python fait beaucoup mieux que les calculatrices, mais aussi que le tableur : le calcul sur les nombres entiers. Là, l’unique limitation de Python [5] est la taille de la mémoire de l’ordinateur utilisé.

Une première application vaut d’être signalée, même si elle n’est pas liée à notre sujet : le célèbre problème des grains de riz sur l’échiquier, dit aussi problème de Sissa. On peut le donner en collège et montrer ainsi les limites du tableur : avec une cellule par case de l’échiquier, les onze dernières contiennent des multiples de 10, alors que, par construction, on ne peut avoir que des puissances de 2. Alors que Python donne instantanément le bon résultat, même si on double la taille de l’échiquier, et au-delà.

Nous allons utiliser cette capacité de Python, pour creuser la question.
Revenons à notre problème de conversion du décimal au binaire, en reprenant la représentation de 0,1 en flottant : $0,1 = (0,00011001100110011...)_2$ tronqué à 56 chiffres après la virgule.
On va pouvoir déterminer l’approximation faite, en n’utilisant que des entiers grâce au module fractions. Celui-ci définit un type « Fraction » sur lequel sont définies les quatre opérations usuelles, entre fractions ou avec des entiers ; la fonction float permet d’obtenir une valeur décimale approchée d’un tel objet.

  1. from fractions import *
  2.  
  3. def undixieme_dec(N):
  4. '''retourne (0,00011001100110011...)_2 tronqué au N-ième chiffre
  5. après la virgule, calculé sous forme de fraction'''
  6. puiss = Fraction(1,2)
  7. i = 1
  8. S = 0
  9. while i < N :
  10. puiss = puiss / 2
  11. i += 1
  12. if i % 4 == 0 or i % 4 == 1 : # le chiffre de rang i est un 1
  13. S += puiss
  14. return(S,float(S))
  15.  

Télécharger

Testons :

>>> undixieme_dec(56)
(Fraction(7205759403792793, 72057594037927936), 0.09999999999999999)

>>> undixieme_dec(57)
(Fraction(14411518807585587, 144115188075855872), 0.1)

Pourtant, on l’a dit, Python tronque au 56ème chiffre, donc pour lui $0,1\simeq \dfrac{7205759403792793}{72057594037927936}$ !

On peut aussi laisser moins de travail à Python, et faire à la main, avec la formule déjà rappelée pour la somme des termes d’une suite géométrique, le calcul du nombre tronqué : $S= \sum_{n=1}^{14}\,\dfrac{1}{2^{4n}} + \sum_{n=1}^{13}\,\dfrac{1}{2^{4n+1}}$ puisque le dernier rang est $56 = 4 \times 14$ . On trouve $\dfrac{2^{56}+2^{55}-9}{15\times 2^{56}}$ qui après simplification par 15 redonne la fraction ci-dessus.

Nous savons donc maintenant quel est le nombre dyadique qui, pour Python, remplace 0.1, et nous connaissons son expression fractionnaire. Mais cela n’éclaire en rien le phénomène d’« explosion » de la suite récurrente. Ce sera l’objet du prochain onglet, où nous utiliserons à nouveau les capacités de Python à calculer avec les entiers.

Pourquoi cette « explosion » ?

Revenons à notre suite $u_{n+1} = (q+1)\,u_n - p$. Pour $p=2, q=3$ par exemple, nous savons maintenant que Python va devoir tronquer l’écriture binaire de $\dfrac{p}{q}$, donc dès le départ introduire une erreur. Que se passe-t-il ensuite ? Continuons d’expérimenter, et toujours avec Python, mais avec le module fractions déjà utilisé, qui ne calcule qu’avec des entiers, donc sans risque d’erreur.
Reprenons le calcul des valeurs de la suite :

  1. from fractions import *
  2.  
  3. def suite_frac(p,q):
  4. u = Fraction(p,q)
  5. for i in range(100): # i varie de 0 à 99
  6. u = (q+1) * u - p
  7. if (i+1) % 10 == 0 : # l'indice mathématique est décalé de 1
  8. approx = float(u)
  9. print('Le terme de rang '+str(i+1)+' vaut : '+str(approx))
  10. print(' écart avec la valeur théorique : ',approx-p/q)

Télécharger

Exécutons ce programme pour $p=2, q=3$ , en gardant seulement le dernier affichage :

>>> suite_frac(2,3)
Le terme de rang 100 vaut : 0.6666666666666666
 écart avec la valeur théorique :  0.0

Python a calculé avec les rationnels donc n’a pas fait d’erreur. Mais avec ce nouvel outil, si nous modifions un peu notre programme pour prendre pour $u_0$ un rationnel très proche mais différent de $\dfrac{p}{q}$, que se passe-t-il ?

  1. def suite_frac_pt_initial(p,q,p0,q0):
  2. '''comme suite_frac mais en partant d'un autre point initial p0/q0 '''
  3. u = Fraction(p0,q0)
  4. print('Valeur initiale : '+str(float(u)))
  5. for i in range(100): # i varie de 0 à 99
  6. u = (q+1) * u - p
  7. if (i+1) % 10 == 0 : # l'indice mathématique est décalé de 1
  8. approx = float(u)
  9. print('Le terme de rang '+str(i+1)+' vaut : '+str(approx))
  10. print(' écart avec la valeur théorique : ',approx-p/q)

Télécharger

Exécutons-le pour $p=2, q=3$ encore, et à partir d’une fraction très proche :

>>> suite_frac_pt_initial(2,3,2000000001,3000000001)
Valeur initiale : 0.6666666667777777
Le terme de rang 10 vaut : 0.6667831751110723
 écart avec la valeur théorique :  0.00011650844440569408
Le terme de rang 20 vaut : 122.83462526772179
 écart avec la valeur théorique :  122.16795860105512
Le terme de rang 30 vaut : 128102390.02472664
 écart avec la valeur théorique :  128102389.35805997
Le terme de rang 40 vaut : 134325091023517.77
 écart avec la valeur théorique :  134325091023517.1
Le terme de rang 50 vaut : 1.4085006664507546e+20
 écart avec la valeur théorique :  1.4085006664507546e+20
Le terme de rang 60 vaut : 1.4769199948242665e+26
 écart avec la valeur théorique :  1.4769199948242665e+26
Le terme de rang 70 vaut : 1.54866286049285e+32
 écart avec la valeur théorique :  1.54866286049285e+32
Le terme de rang 80 vaut : 1.6238907076041507e+38
 écart avec la valeur théorique :  1.6238907076041507e+38
Le terme de rang 90 vaut : 1.70277282261673e+44
 écart avec la valeur théorique :  1.70277282261673e+44
Le terme de rang 100 vaut : 1.7854867152481602e+50
 écart avec la valeur théorique :  1.7854867152481602e+50

Ainsi l’explosion se produit même en calcul exact, dès lors que la valeur initiale s’écarte un peu de celle attendue.

En fait, c’est un phénomène classique pour ces suites récurrentes du type $u_{n+1} = f(u_n)$ et qui n’a rien à voir avec Python. Voyons ce qui peut se passer pour une telle suite [6]. Nous supposerons la fonction $f$ dérivable. Si notre suite récurrente a une limite, disons $l$, en passant à la limite dans la relation qui définit $(u_n)$ on a par continuité de $f$ l’égalité $l=f(l)$. Ainsi $l$ doit être un point fixe de $f$.

Pour mieux voir ce qui peut se passer, nous allons étudier le cas de la fonction $f,\quad f(x) = \dfrac{x^2}{2}-3\dfrac{x}{2}+2$ : elle a deux points fixes (et deux seulement) en $x=1$ et $x=4$. Le programme qui suit va nous permettre de visualiser le phénomène.

  1. import matplotlib.pyplot as plt
  2.  
  3. def f(x):
  4. '''la fonction qu'on veut representer'''
  5. return(x**2/2 - 3*x/2 + 2)
  6.  
  7. def graphe(f,a,b,N):
  8. '''trace le graphe de la fonction f entre a et b avec N segments'''
  9. lx = [a+i*(b-a)/N for i in range(N+1)]
  10. ly = [f(x) for x in lx]
  11. return(plt.plot(lx,ly,'m',label ="$f$"))
  12.  
  13. def suite_rec(f,u0,a,b,N):
  14. '''dessine sur [a,b] la suite des segments reliant les points qui
  15. representent la suite u_{n+1}=f(u_n) partant de u0 avec N termes'''
  16. u,v = u0,0
  17. # on crée les listes des abscisses et ordonnées des segments
  18. lx = [u] ; ly = [v]
  19. v = f(u) ; lx.append(u) ; ly.append(v) # premier segment vertical
  20. for i in range(N):
  21. u = v ; lx.append(u) ; ly.append(u) # segment horizontal
  22. v = f(u) ; lx.append(u) ; ly.append(v) # segment vertical
  23. plt.plot(lx,ly,'r',label = "$u_{n+1} =f(u_n)$")
  24. # on trace la droite d'équation y = x
  25. plt.plot([a,b],[a,b],'b',label = "$y=x$")
  26. #on trace le graphe de f sur le même intervalle
  27. graphe(f,a,b,30)
  28. plt.legend(loc='upper left') # ces legendes doivent avoir ete creees (option 'label' de plot)
  29. plt.title("$u_{0} = $ "+str(u0)+", $N = $"+str(N))
  30. # on ajoute les axes
  31. plt.axhline(color = 'k')
  32. plt.axvline(color='k')
  33. # et une grille
  34. plt.grid()
  35. # on affiche !
  36. plt.show()

Télécharger

Un premier test :

Un second :

Nous invitons le lecteur à télécharger le programme et à poursuivre les tests s’il le souhaite. Pour résumer, le point fixe 1 est « attractif » ($|f’(1)| < 1$), l’autre point fixe 4 est « répulsif » ($|f’(4)| > 1$) : pour une valeur initiale $u_0$ même proche de 4 (mais inférieure à 4), c’est vers 1 que la suite va converger. Pour $u_0$ plus grand que 4, elle diverge, et très rapidement :

C’est exactement ce qui se passe pour le phénomène dont nous étions partis, de divergence massive de la suite $(u_n)_n$ définie par la relation de récurrence $u_{n+1} = (q+1)\,u_n - p$ pour certaines valeurs du premier terme $u_0=\dfrac{p}{q}$ : ici la fonction $f$ est affine : $f(x)=(q+1)\,x - p$ donc sa dérivée est partout $q+1$ qui, pour les valeurs que nous avions prises, est plus grand que 1.

Pour nous résumer, à ce stade : ce problème semble en fait dû à la combinaison de deux phénomènes : l’un, informatique, est la conversion de la fraction en binaire ; l’autre, mathématique, est l’instabilité du "système dynamique" $u_{n+1} = f(u_n)$ au voisinage d’un point $a$ où $|f’(a)|\geq 1$ : Python prend une valeur approchée de $u_0$ et cela suffit pour faire diverger la suite.

Pour une dernière vérification, nous allons reprendre le même problème et notre premier programme, mais en modifiant $f$ de façon à trouver un système dynamique stable. Pour le cas $p=2, q=3$ par exemple, il suffit de prendre $p’=-\dfrac{1}{3}$ et $q’=-\dfrac{1}{2}$ de sorte que le premier terme $u_0=\dfrac{p’}{q’}$ vaut toujours $\dfrac{2}{3}$, mais cette fois la dérivée de $f$ est $q’+1=\dfrac{1}{2}$. On obtient :

>>> suite(-1/3,-1/2)
Le terme de rang 100 vaut : 0.6666666666666666
 écart avec la valeur théorique :  0.0

C’est bien ça : Python fait toujours une approximation en convertissant en binaire, mais comme le point fixe $\dfrac{2}{3}$ est maintenant attractif, on ne s’écarte pas de cette valeur et la suite apparaît constante.

Et les calculatrices ?

La calculatrice NumWorks a un module Python intéressant et régulièrement amélioré. Et c’est un « vrai » Python, la preuve : à nos deux questions de départ, 0.1 + 0.1 + 0.1 - 0.3 et la suite récurrente, il a exactement les mêmes réponses (fausses) qu’un ordinateur ! De même lorsqu’on lui demande si le triangle de côtés $\sqrt{3}, \sqrt{5}$ et $2\sqrt{2}$ est rectangle.

Mais si l’on demande 0.1+0.1+0.1-0.3 au module « calculs » de cette même machine, la réponse est bien 0, et notre triangle de test est bien rectangle.

Sur une (assez ancienne) TI-Nspire CAS, en mode « calcul exact », toutes les réponses sont mathématiquement correctes, même pour notre suite récurrente : il n’y a pas d’approximation numérique, donc en partant de $\dfrac{p}{q}$ la suite reste constante. La calculatrice fait ici du calcul formel et non numérique. En passant en mode « approché », la suite récurrente diverge comme avec l’ordinateur, par contre les réponses restent correctes pour 0.1+0.1+0.1-0.3 et pour le triangle rectangle test.

En fait, les calculatrices utilisent le système « décimal codé binaire » (DCB) : pour un nombre entré en système décimal, chacun de ses chiffres est converti en binaire, et les calculs sont menés en traitant séparément chaque bloc de « bits » représentant un chiffre décimal. Ainsi par exemple 0.1 est représenté exactement, sans troncature ni arrondi. Et les calculs sont menés, de fait, en système décimal. Ce codage DCB est peu performant en termes d’occupation de la mémoire : il faut quatre bits pour coder chaque chiffre décimal, mais avec ces quatre bits on a $2^4= 16$ valeurs possibles, et là on n’en utilise [7] que 10. Mais pour les systèmes qui, comme les calculatrices, ne sont utilisés que sur des nombres et avec un format décimal pour l’entrée et la sortie, il a l’avantage de la simplicité.

La calculatrice reste cependant limitée par le nombre de chiffres avec lesquels elle travaille, 14 pour la TI. Donc pour notre suite récurrente, comme elle remplace (en mode « approché », toujours) $\dfrac{2}{3}$ par 0.66666666666667, cela suffit pour faire diverger le calcul, à cause de l’instabilité du système. On peut le vérifier avec notre petit programme de l’onglet précédent : pour suite_frac_pt_initial(2,3,p0,q0) avec $p_0=6666666666667$ et $q_0=10^{13}$, le centième terme vaut $5.4 \times 10^{46}$ pour Python et $1.3 \times 10^{46}$ pour la TI : l’ordre de grandeur est le même mais Python a calculé avec plus de chiffres.

Il est important de souligner ici que le problème présenté n’est en rien particulier au langage Python : comme on l’a montré, la cause est la traduction en langage binaire, commune à tous les langages utilisés pour le calcul numérique. L’utilisateur doit être conscient de cette particularité, d’abord pour respecter des précautions élémentaires : déjà, comme on l’a dit, ne jamais tester l’égalité de deux flottants. Mais aussi parce que ses conséquences peuvent être bien plus dramatiques que la divergence d’une suite qui devrait converger, voir par exemple ici. Les mathématiques, plus précisément l’analyse numérique, peuvent souvent dire dans quelles conditions un système risque ou non de diverger. Mais cela ne dispense pas, pour tout calcul confié à une machine, de contrôler à l’arrivée la vraisemblance du résultat...


notes

[1Certains lecteurs seront peut-être choqués par l’utilisation de "print" au lieu de "return" dans cette fonction. Mais il s’agit d’afficher plusieurs valeurs successives. Bien sûr nous pourrions ranger ces valeurs dans une liste, et retourner la liste. Mais cela encombrerait la mémoire, et inutilement, puisque nous ne ferons aucun autre usage de ces valeurs.

[2Pour être plus précis : unique, sauf pour les nombres décimaux, qui ont tous exactement deux écritures, par exemple $2,34=2,33\overline{9}$, le 9 étant répété à l’infini.

[3Au même sens qu’à la note précédente, mais en base 2 ! par exemple, $\dfrac{1}{2}=(0,1)_2=\sum_{n=2}^{+\infty}\,\dfrac{1}{2^{n}}=(0,0\overline{1})_2$.

[4Je tire cet exemple des ressources préparées collectivement à l’occasion de la formation académique 2017/2018 coordonnée par P. Marquet dans l’académie de Lille. Je remercie les collègues pour les nombreux échanges autour de cette préparation, et particulièrement, sur le sujet de ce texte, É. Wegrzynowski (université de Lille).

[5du moins, à partir de sa version 3, très majoritaire maintenant.

[6Nous nous en tenons ici à une approche expérimentale. Les aspects mathématiques sont traités par exemple dans
cette référence et de façon plus élémentaire dans celle-ci. Je remercie C. De Coster pour ces sources et nos échanges à ce sujet.

[7Comme expliqué dans l’article de Wikipédia auquel nous renvoyons, cela peut même être bien pire, si le « format étendu » est utilisé.

Réagir à cet article
Vous souhaitez compléter cet article pour un numéro futur, réagir à son contenu, demander des précisions à l'auteur ou au comité de rédaction...
À lire aussi ici
MathémaTICE est un projet
en collaboration avec
Suivre la vie du site Flux RSS 2.0  |  Espace de rédaction  |  Nous contacter  |  Site réalisé avec: SPIP  |  N° ISSN 2109-9197