11 avril 2020 — Introduction, Python
Nous avons vu ce que sont les entiers naturels , 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 et le nombre correspondant à parts de gâteaux coupés en 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 par justifie que : .
Pour cette raison, on peut donc toujours réduire une fraction en éliminant les diviseurs communs et supposer que ( et 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é .
L’ensemble partage certaines propriétés avec :
Chaque nombre non nul dans (notons le , avec ) a un inverse pour la multiplication : (propriété qui n’est pas vraie dans ).
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 , qui est celui des nombres que nous manipulons habituellement.
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)
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 où et .
Si n’est pas divisible par , le nombre correspond au nombre de chiffres après la virgule dans l’écriture décimale de .
Par exemple :
Un nombre rationnel peut toujours s’approximer par un nombre décimal avec chiffres après la virgule, l’erreur d’approximation étant alors inférieure à .
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.denominatoryield qif r == 0:breakx = 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 . Par exemple, si 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 :
Considérons la suite de nombre rationnels , définie par la formule récurrente suivante :
et
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_0for i in range(n):x = (x + 2 / x) / 2yield x
>>> for x in magic_sequence(5):... print(x)...3/217/12577/408665857/470832886731088897/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 :
À 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 , soit par exemple en calculant avec décimales :
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 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 : 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 par des rationnels correspond à une idée très ancienne. C’est la méthode de Héron.
Il n’existe pas de nombres entiers et tels que , autrement dit .
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 et comme produits de nombres premiers, en isolant le facteur correspondant à et les autres facteurs, dont on note les produits et :
et
On a alors :
Soit encore :
Cette dernière égalité est impossible car par unicité de la décomposition en facteurs premiers on aurait alors (égalité entre un nombre impair et un nombre pair).
💡 Le résultat et sa preuve restent valables en remplaçant par n’importe quel autre nombre premier.
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 :
(20 décimales) | |
---|---|
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 | |
… | … |
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 comme la suite des développements décimaux obtenus: , , , etc.
Cette première approche est correcte mathématiquement, tout nombre réel pouvant être approché par des développements décimaux infinis.
Cette définition donne cependant un rôle particulier à la base , 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 :
>>> 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 :
Une définition équivalente des nombres réels , 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 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 .
En quel sens les valeurs se rapprochent-elles ?
Si l’on considère une distance , aussi petite soit-elle, à partir d’un certain indice les éléments de la suite seront tous à une distance inférieure à les uns des autres. En voici la traduction mathématique :
Si l’on fixe une base numérique, les suites de Cauchy de 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 , si l’on considère , 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 :
On peut définir comme l’ensemble des suites de Cauchy de nombres rationnels, regroupées par suites « équivalentes » :
si la différence entre deux suites converge vers , alors elles sont dites équivalentes et définissent un seul et même nombre réel (par ex. ).
Le nombre peut ainsi être défini par n’importe laquelle de ces suites (de Cauchy) :
magic_sequence
étudiée au départDans tous les cas, les suites obtenues sont équivalentes et définissent le même nombre de .
Avant d’étudier les problématiques liées aux nombres en informatique, résumons ce que l’on a vu:
Le cadre mathématique définit, avec les nombres réels , les nombres que l’on s’attend à pouvoir manipuler.
On s’attend ainsi à pouvoir manipuler informatiquement des nombres comme , , , , 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.pi3.141592653589793>>> math.sqrt(2)1.4142135623730951>>> 1/30.3333333333333333
L’utilisation de ces nombres présente cependant des limites, conséquences de deux contraintes fondamentales du monde de l’informatique :
Ceci peut avoir des conséquences inattendues.
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 ) ou non (représentent des nombres de ).
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.
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.
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 , ou , 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 structfrom math import pi, sqrtdef 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 .
La table suivante donne cette décomposition pour les exemples précédents :
x | Signe | Exposant | Mantisse |
---|---|---|---|
42 | 0 | 10000000100 | 0101000000000000000000000000000000000000000000000000 |
0 | 10000000000 | 1001001000011111101101010100010001000010110100011000 | |
0 | 01111111111 | 0110101000001001111001100110011111110011101111001101 | |
0 | 01111111011 | 1001100110011001100110011001100110011001100110011010 |
Quelques remarques :
Le cas de 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 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 dans son terminal est le suivant :
Très proche, mais pas identique.
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.20.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 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