Lipsum.dev

De la construction des réels à la norme IEEE 754

11 avril 2020 — Introduction, Python

Fractions et nombres rationnels

Nous avons vu ce que sont les entiers naturels N\N, qui nous permettent de compter des quantités « avec les doigts ». Si maintenant, nous souhaitons compter des quantités non-entières, comme des parts de gâteaux, et pouvoir les comparer, il faut introduire des fractions.

On définit ainsi pour deux entiers mZm \isin \Z et nN,n0n \isin \N, n \neq 0 le nombre mnm \over n correspondant à mm parts de gâteaux coupés en nn parts égales.

Si l’on prend un gâteau coupé en parts deux fois plus petites, il faut en servir deux fois plus pour arriver à la même quantité, et le même raisonnement en remplaçant 22 par kN,k0k \isin \N, k \neq 0 justifie que : mn=k×mk×n\frac{m}{n} = \frac{k \times m}{k \times n}.

Pour cette raison, on peut donc toujours réduire une fraction en éliminant les diviseurs communs et supposer que gcd(m,n)=1\gcd(m,n) = 1 (mm et nn sont dits premiers entre eux).

Les nombres que l’on peut ainsi écrire sous forme de fraction s’appellent les nombres rationnels et forment un ensemble noté Q\Bbb{Q}.

Notion de corps

L’ensemble Q\Bbb{Q} partage certaines propriétés avec Z\Z :

  • il est muni d’une addition : ab+cd=a×d+c×bb×d{\cfrac{a}{b} + \cfrac{c}{d} = \cfrac{a \times d + c \times b}{b \times d}}
  • chaque nombre xx dans Q\Bbb{Q} a un opposé, noté x-x
  • il est muni d’une multiplication : ab×cd=a×cb×d{\cfrac{a}{b} \times \cfrac{c}{d} = \cfrac{a \times c}{b \times d}}

Chaque nombre non nul dans Q\Bbb{Q} (notons le aba \over b, avec a0a \neq 0) a un inverse pour la multiplication : (ab)1=ba(\cfrac{a}{b})^{-1} = \cfrac{b}{a} (propriété qui n’est pas vraie dans Z\Z).

Ces propriétés sont caractéristiques d’une catégorie d’ensembles que les mathématiciens appellent les corps.

💡 La notion de corps est au coeur des mathématiques et de ses applications. Les corps finis sont notamment utilisés en cryptographie et le corps des nombres complexes que nous verrons plus tard est au coeur de la physique.

Dans la suite, nous allons construire le corps des nombres réels R\R, qui est celui des nombres que nous manipulons habituellement.

💻 Les rationnels en Python

Nous allons utiliser le module fractions, qui fait partie de la bibliothèque standard de Python. Son usage reste assez limité dans la pratique, mais il est intéressant d’un point de vue didactique.

Ce module définit notamment un type Fraction qui permet d’effectuer des calculs sur les nombres rationnels (comme nous le verrons, les nombres flottants ne permettent que du calcul approximatif, certaines fractions se retrouvant automatiquement tronquées).

from fractions import Fraction
>>> Fraction(19, 23) + Fraction(13, 42)
Fraction(1097, 966)

Développements en base décimale, binaire, …

Les nombres décimaux sont les fractions qui peuvent s’écrire avec un dénominateur qui est une puissance de 10, c’est-à-dire sous la forme x=m10dx = \cfrac{m}{10^d}mZm \isin \Z et dNd \isin \N.

Si mm n’est pas divisible par 1010, le nombre dd correspond au nombre de chiffres après la virgule dans l’écriture décimale de xx.

Par exemple : 389532103=389.532\cfrac{389532}{10^3} = 389.532

Un nombre rationnel peut toujours s’approximer par un nombre décimal avec dd chiffres après la virgule, l’erreur d’approximation étant alors inférieure à 10d10^{-d}.

L’algorithme suivant permet d’obtenir ce développement, sous la forme de la partie entière puis des premiers chiffres décimaux :

def decimals_dev(x: Fraction, digits=10):
for _ in range(digits + 1):
q, r = x.numerator // x.denominator, x.numerator % x.denominator
yield q
if r == 0:
break
x = Fraction(10 * r, x.denominator)

On a :

>>> list(decimals_dev(Fraction(987654321, 16)))
[61728395, 0, 6, 2, 5]
>>> list(decimals_dev(Fraction(1, 3), digits = 20))
[0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]

De la même façon, on peut écrire le développement d’un nombre rationnel en base n>0n > 0. Par exemple, si n=2n = 2 il s’agit du développement binaire.

💻 Modifier le code précédent pour obtenir la fonction binary_dev qui donne le développement binaire d’un nombre.

On observe :

>>> list(binary_dev(Fraction(1, 2)))
['0', '1']
>>> list(binary_dev(Fraction(343, 16)))
['10101', '0', '1', '1', '1']
>>> list(binary_dev(Fraction(1, 10)))
['0', '0', '0', '0', '1', '1', '0', '0', '1', '1', '0']

💡 On notera en particulier que tous ces développements présentent une périodicité : on peut montrer que les développpements des nombres rationnels sont toujours constitués de séquences qui se répètent, quelle que soit la base utilisée.

Questions :

  • À quelle condition sur le dénominateur d’un nombre rationnel, son développement binaire est-il fini (nombre de chiffres fini) ?
  • Montrer qu’un nombre rationnel dont le développement binaire est fini admet un développement décimal fini, mais que la réciproque n’est pas vraie.
  • Dans quelle base le développement de 131 \over 3 sera-t-il fini ?

Une séquence mystérieuse

Considérons la suite de nombre rationnels (xn)n0(x_n)_{n \geq 0}, définie par la formule récurrente suivante :

x0=1x_0 = 1 et xn+1=12(xn+2xn)x_{n+1} = \frac{1}{2} (x_n + \cfrac{2}{x_n})

Le programme Python suivant va nous aider à calculer les premières valeurs de cette suite :

def magic_sequence(n, x_0=Fraction(1)):
x = x_0
for i in range(n):
x = (x + 2 / x) / 2
yield x
>>> for x in magic_sequence(5):
... print(x)
...
3/2
17/12
577/408
665857/470832
886731088897/627013566048

On constate que cette suite prend pour valeurs des fractions avec un dénominateur toujours plus grand — c’est-à-dire avec une précision toujours plus grande, par exemple :

x7=49460411762552018787750864875733510614189684981773497379255757941172020851852070562919437964212608x_7 = \frac{4946041176255201878775086487573351061418968498177}{3497379255757941172020851852070562919437964212608}

À quoi peuvent bien ressembler les développements décimaux de ces nombres ? Nous savons le déterminer simplement :

>>> for x in magic_sequence(5):
... print(list(decimals_dev(x, digits=20)))
...
[1, 5]
[1, 4, 1, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6]
[1, 4, 1, 4, 2, 1, 5, 6, 8, 6, 2, 7, 4, 5, 0, 9, 8, 0, 3, 9, 2]
[1, 4, 1, 4, 2, 1, 3, 5, 6, 2, 3, 7, 4, 6, 8, 9, 9, 1, 0, 6, 2]
[1, 4, 1, 4, 2, 1, 3, 5, 6, 2, 3, 7, 3, 0, 9, 5, 0, 4, 8, 8, 0]

Quel est donc ce nombre mystérieux dont on obtient le développement décimal ?

Les observateurs avisés auront reconnu une approximation de 2\sqrt{2}, soit par exemple en calculant x7x_7 avec 5050 décimales :

21.41421356237309504880168872420969807856967187537694\sqrt{2} \approx 1.41421356237309504880168872420969807856967187537694

Mais ce nombre en est-il vraiment un ? Et si oui, en quel sens ?

La suite précédente nous montre que si on veut s’approcher d’un nombre dont le carré est 22 avec des rationnels, il faut considérer des fractions toujours plus précises (c’est-à-dire, avec un dénominateur toujours plus grand).

En fait, c’est la conséquence d’un résultat fondamental : 2\sqrt{2} n’existe pas sous forme de fraction, mais on peut trouver une suite de fractions qui converge vers ce nombre.

💡 La suite utilisée précédemment pour approximer 2\sqrt{2} par des rationnels correspond à une idée très ancienne. C’est la méthode de Héron.

Pas de 2\sqrt{2} dans Q\Bbb{Q}

Il n’existe pas de nombres entiers mm et n0n \neq 0 tels que (mn)2=2{(\frac{m}{n})^2 = 2}, autrement dit 2Q{\sqrt{2} \notin \Bbb{Q}}.

Ce résultat très classique peut se prouver en utlisant la décomposition unique d’un entier en facteurs premiers, vue à l’article précédent.

On écrit mm et nn comme produits de nombres premiers, en isolant le facteur correspondant à 22 et les autres facteurs, dont on note les produits aa et bb :

m=2i×a{m = 2^i \times a} et n=2j×b{n = 2^j \times b}

On a alors : 2=(mn)2=22i×a222j×b2{2 = (\frac{m}{n})^2 = \cfrac{2^{2i} \times a^2}{2^{2j} \times b^2}}

Soit encore : 22j+1×b2=22i×a2{2^{2j + 1} \times b^2 = 2^{2i} \times a^2}

Cette dernière égalité est impossible car par unicité de la décomposition en facteurs premiers on aurait alors 2j+1=2i{2j + 1 = 2i} (égalité entre un nombre impair et un nombre pair).

💡 Le résultat et sa preuve restent valables en remplaçant 22 par n’importe quel autre nombre premier.

Les nombres réels R\R

Définition par développements décimaux infinis

Une intuition classique pour définir les nombres réels, est de considérer qu’ils sont définis comme limite d’une suite infinie de nombres décimaux.

Dans notre suite étudiée précédemment, on a ainsi obtenu pour 2\sqrt{2} :

nnxnx_n (20 décimales)
111, 5
221, 4 1 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6 6
331, 4 1 4 2 1 5 6 8 6 2 7 4 5 0 9 8 0 3 9 2
441, 4 1 4 2 1 3 5 6 2 3 7 4 6 8 9 9 1 0 6 2
551, 4 1 4 2 1 3 5 6 2 3 7 3 0 9 5 0 4 8 8 0

Ci-dessus, on observé que la partie en gras du développement décimal reste figée (c’est-à-dire, elle est la même dans les itérations suivantes).

On peut alors définir 2\sqrt{2} comme la suite des développements décimaux obtenus: 11, 1.41.4, 1.4141.414, etc.

Cette première approche est correcte mathématiquement, tout nombre réel pouvant être approché par des développements décimaux infinis.

Choix de base arbitraire

Cette définition donne cependant un rôle particulier à la base 1010, alors qu’un nombre réel représente une quantité qui n’est intrinsèquement liée à aucune base.

Par exemple, on peut aussi définir un nombre réel comme une limite de développements binaires. Par exemple pour 2\sqrt{2} :

>>> for x in magic_sequence(5):
... print(list(binary_dev(x, digits=20)))
...
[1, 1]
[1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0]
[1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0]
[1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0]
[1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0]

D’où le développement approché en binaire :

21.011010100000100111102\sqrt{2} \approx 1.01101010000010011110_2

Définition par les suites de Cauchy

Une définition équivalente des nombres réels R\R, plus générique, utilse des suites avec une propriété particulière, qu’on appelle des suites de Cauchy.

Intuitivement, une suite de Cauchy dans Q\Bbb{Q} est une suite dont les valeurs se rapprochent de plus en plus, comme pour une suite convergente — mais dont on ne peut pas toujours exprimer de limite dans Q\Bbb{Q}.

En quel sens les valeurs se rapprochent-elles ?

Si l’on considère une distance ϵQ\epsilon \isin \Bbb{Q}, aussi petite soit-elle, à partir d’un certain indice NN les éléments de la suite seront tous à une distance inférieure à ϵ\epsilon les uns des autres. En voici la traduction mathématique :

ϵ>0,NN,m,n>N,xmxn<ϵ{\forall \epsilon > 0, \exists N \isin \N, \forall m, n \gt N, |x_m - x_n| < \epsilon}

Si l’on fixe une base numérique, les suites de Cauchy de Q\Bbb{Q} sont caractérisées par la propriété suivante : leurs premiers chiffres (digits) finissent toujours par se figer quand on itère.

Par exemple, en base 1010, si l’on considère ϵ=0.001\epsilon = 0.001, la définition nous garantit que la suite finira par prendre des valeurs dont tous les chiffres jusqu’à la deuxième décimale coincident.

Ainsi, toutes les suites que l’on a étudiées précédemment sont de Cauchy :

  • Notre suite convergeant vers 2\sqrt{2} car on a vu que ses décimales se figent progressivement
  • Toute suite des développements décimaux d’un nombre quelconque (par construction, les premières décimales sont figées)
  • Toute suite des développements binaires d’un nombre quelconque (même argument)

On peut définir R\R comme l’ensemble des suites de Cauchy de nombres rationnels, regroupées par suites « équivalentes » :

si la différence entre deux suites converge vers 00, alors elles sont dites équivalentes et définissent un seul et même nombre réel (par ex. 1.000...=0.999...1.000... = 0.999...).

Le nombre 2\sqrt{2} peut ainsi être défini par n’importe laquelle de ces suites (de Cauchy) :

  • la suite de ses développements décimaux
  • celle de ses développements binaires
  • la suite définie par notre magic_sequence étudiée au départ
  • par cette même suite, mais pour laquelle on change la valeur de x0x_0 (11 dans notre exemple, mais on aurait pu prendre 22 ou 100100, ce qui aurait simplement affecté la vitesse de convergence)

Dans tous les cas, les suites obtenues sont équivalentes et définissent le même nombre de R\R.

Petit résumé

Avant d’étudier les problématiques liées aux nombres en informatique, résumons ce que l’on a vu:

  • On part des nombres entiers naturels N\N, qui servent à « compter avec les doigts ».
  • On construit les entiers relatifs Z\Z, qui expriment des différences entre deux nombres, positives ou négatives.
  • Pour pouvoir compter des « parts de gâteaux », non entières, on construire le corps des fractions Q\Bbb{Q}.
  • On constate que certains nombres, par exemple 2\sqrt{2}, n’existent pas comme fractions mais qu’on peut constuire des suites de fractions qui les approximent de façon toujours plus précise.
  • On introduit la notion de suite de Cauchy, ces suites dont les termes se rapprochent toujours plus.
  • R\R est l’ensemble des suites de Cauchy à valeurs dans Q\Bbb{Q}.
    Celles qui sont équivalentes (leur différence tend vers 00) définissent le même nombre.
  • R\R peut aussi être vu comme l’ensemble des développements décimaux infinis, mais la définition précédente évite la référence à une base particulière.

💻 Et en informatique ?

Le cadre mathématique définit, avec les nombres réels R\R, les nombres que l’on s’attend à pouvoir manipuler.

On s’attend ainsi à pouvoir manipuler informatiquement des nombres comme 2323, 131 \over 3, π\pi, 2\sqrt{2}, etc.

Dans la pratique, ces nombres sont d’ailleurs généralement accessibles via la bibliothèque standard des langages, par ex.:

>>> import math
>>> math.pi
3.141592653589793
>>> math.sqrt(2)
1.4142135623730951
>>> 1/3
0.3333333333333333

L’utilisation de ces nombres présente cependant des limites, conséquences de deux contraintes fondamentales du monde de l’informatique :

  • Les données sont stockées et circulent sous forme binaire (en RAM, sur disque, dans les câbles, etc.)
  • La mémoire est finie. En particulier, le nombre de bits alloués par défaut pour un nombre est généralement limité.

Ceci peut avoir des conséquences inattendues.

Représentation des entiers

Il y aurait beaucoup à dire sur le cas des entiers, qui sont représentés en mémoire à l’aide d’un nombre fixe de chiffres binaires (8, 16, 32, 64, …), et prennent donc leurs valeurs dans une plage limitée. Ces limites dépendent également de si ces entiers sont signés (représentent des nombres de Z\Z) ou non (représentent des nombres de N\N).

Ces représentations de taille fixée induisent des limites qu’il faut connaître, en particulier lorsque l’on utilise un langage bas niveau comme le C.

Les entiers en Python 3

Python implémente les entiers par des objets de taille variable, la mémoire étant allouée en fonction du nombre de chiffre nécessaires pour permettre une précision arbitraire (la seule limite étant alors la mémoire disponible à l’allocation).

Contrairement au C, il n’existe donc pas de limite du type INT_MAX en Python.

Pour en savoir plus sur les subtilités de l’implémentation des entiers en Python, je vous renvoie à cet article.

Représentation des flottants

Contrairement aux entiers, les nombres réels ne sont représentés en Python que de façon approximative. Il serait de toute façon illusoire d’espérer pouvoir stocker en mémoire toutes les décimales de 131 \over 3, π\pi ou 2\sqrt{2}, puisqu’il y en a une infinité.

La fonction suivante, qui utilise le module struct de Python, permet d’obtenir la représentation binaire d’un nombre flottant :

import struct
from math import pi, sqrt
def show_binary_rep(num):
print('{:064b}'.format(*struct.unpack(">Q", struct.pack(">d", num))))
>>> show_binary_rep(42)
0100000001000101000000000000000000000000000000000000000000000000
>>> show_binary_rep(pi)
0100000000001001001000011111101101010100010001000010110100011000
>>> show_binary_rep(sqrt(2))
0011111111110110101000001001111001100110011111110011101111001101
>>> show_binary_rep(1/10)
0011111110111001100110011001100110011001100110011001100110011010

On observe en particulier que ces nombres sont représentés sur 64 bits. Concrètement, Python comme beaucoup de langages utilise la norme IEEE 754 et la précision double pour la représentation des nombres réels. Ce n’est qu’une représentation approximative.

Cette représentation binaire se compose en fait de 3 parties : 1 bit de signe, 11 bits d’exposant et 52 bits pour la mantisse (chiffres significatifs).

Elle correspond à l’écriture d’un nombre sous la forme x=±1×2exposant×mantisse{x = \pm 1 \times 2^{exposant} \times mantisse}.

La table suivante donne cette décomposition pour les exemples précédents :

xSigneExposantMantisse
420100000001000101000000000000000000000000000000000000000000000000
π\pi0100000000001001001000011111101101010100010001000010110100011000
2\sqrt{2}0011111111110110101000001001111001100110011111110011101111001101
0.10.10011111110111001100110011001100110011001100110011001100110011010

Quelques remarques :

  • L’exposant est signé (voir la règle du complément à deux)
  • Le premier chiffre significatif de la mantisse est toujours omis (il est implicite).
    Ainsi 42=00101010=22×1.010100042 = 00101010 = 2^2 \times 1.0101000 mais le premier 11 n’apparait pas.
  • Les développements de π\pi et 2\sqrt{2} sont tronqués, donc approximatifs (ce sont des nombres irrationnels, donc ils ne peuvent certainement pas s’écrire avec un nombre de chiffres finis, quelle que soit la base).

Le cas de 0.1=1100.1 = {1 \over 10} est plus subtil.

Ce nombre s’écrit en base décimale de façon exacte avec un chiffre significatif. Son développement binaire est en revanche 0.000110011001100...0.000110011001100... avec une séquence qui se répète à l’infini. Sa représentation tronquée est donc forcément approximative.

En fait, le nombre stocké en mémoire lorsque l’on saisit 0.10.1 dans son terminal est le suivant : 0.10000000000000000555111512312578270211815834045410156250.1000000000000000055511151231257827021181583404541015625

Très proche, mais pas identique.

Quelles conséquences pratiques ?

Cette approximation a des conséquences qu’il est important d’avoir en tête lorsque l’on manipule des nombres, en particulier pour des quantités que l’on voudrait exacte (montant de transaction, etc.).

Par exemple en Python (je vous laisse vérifier ce résultat, dans la plupart des langages classiques, utilisant la même norme) :

>>> 0.1 + 0.2
0.30000000000000004

😯

Que s’est-il passé ?

Ce résultat surprenant est simplement la conséquence de la valeur approximative des deux termes sommés.

Il est plus grand que 33 car les représentations binaires sont arrondies vers le haut, lorsqu’elles sont tronquées.

De façon pratique, lorsque l’on souhaite manipuler des montants décimaux en Python (valeurs monétaires, etc.), on privilégiera l’utilisation de types dédiés comme Decimal (voir aussi la doc. Python à ce sujet).

from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2')
Decimal('0.3')


Maths et applications, avec les mains et avec du code 💻
Suivre sur Twitter