Les nombres

Une vidéo de présentation des nombres dans Python…

Les valeurs numériques

Python permet de manipuler trois types numériques : les entiers (type int), les réels (type float) et les complexes (complex). type retourne le type de l’objet passé en argument:

>>> type(1)
<class 'int'>

>>> type(1.0)
<class 'float'>

>>> type(1+1j)
<class 'complex'>

En Python nul besoin de déclarer le type des objets manipulés. Python utilise un mécanisme d”inférence de type.

Lorsque plusieurs types différents sont mis en jeu, les opérations mathématiques standard retournent le résultat dans le type qui conserve la précision.

Une opération mélant un int et un float retournera un float:

>>> type(1 + 1.0)
<class 'float'>

Une opération mélant un float et un complex retournera un complex:

>>> type(1.0 + 1+2j)
<class 'complex'>

A expérimenter…

Utiliser l’interpréteur interactif pour vérifier votre intuition sur le résultat retourné par une opération arithmétique mélant un int et un un complex.

Python est un langage entièrement objet. Chacun des objets numériques manipulé ci dessus est une instance d’une des classes int, float et complex. Chacune de ces classes possède un constructeur portant le même nom que la classe. Celui ci a pour but, lorque c’est possible, de construire une instance de la classe à partir du paramètre passé en argument:

>>> int(1)
1

>>> int(1.0)
1

Lorsque l’opération n’est pas possible, une erreur se produit:

>>> int(1+1j)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't convert complex to int

Les messages d’erreur de Python sont relativement explicites :

  • la dernière ligne fournit une explication en langage naturel ;

  • l’avant dernière ligne localise l’erreur dans la séquence d’instructions exécutées.

La précision

Contrairement à d’autres langages la précision des entiers et des rééls n’est pas fixée a priori mais adaptée dynamiquement aux résultats du calcul et ne dépend que de la taille de la mémoire.

L’ensemble des modules disponibles n’est pas chargé en mémoire au démarrage de l’interpréteur pour des raisons évidentes de performances (temps de chargement et d’occupation mémoire). Seuls les modules de base sont importés au démarrage de Python. Si d’autres sont nécessaires, on les importe à la demande. Le module math fait partie de ceux là. Il est chargé avec l’instruction import:

>>> import math

Ce module dispose d’un grand nombre de fonctions disponibles, en particulier la fonction factorial():

>>> math.factorial(5)
120

Parce que les nombres sont codés sur un nombre statique de bits, la plupart des langages ne peuvent pas calculer de factorielle supérieure à 20 (\(20 ! = 2432902008176640000\)) sans utiliser de modules spécialisés. Python utilise dynamiquement la mémoire pour allouer la taille nécessaire

>>> math.factorial(50)
30414093201713378043612608166064768844377641568960512000000000000

Bien qu’il n’y ait absolument aucun intérêt à manipuler un si grand nombre, pour illustrer ce phénomène

>>> math.factorial(500)
1220136825991110068701238785423046926253574342803192842192413588385845373153881997605496447502203281863013616477148203584163378722078177200480785205159329285477907571939330603772960859086270429174547882424912726344305670173270769461062802310452644218878789465754777149863494367781037644274033827365397471386477878495438489595537537990423241061271326984327745715546309977202781014561081188373709531016356324432987029563896628911658974769572087926928871281780070265174507768410719624390394322536422605234945850129918571501248706961568141625359056693423813008856249246891564126775654481886506593847951775360894005745238940335798476363944905313062323749066445048824665075946735862074637925184200459369692981022263971952597190945217823331756934581508552332820762820023402626907898342451712006207714640979456116127629145951237229913340169552363850942885592018727433795173014586357570828355780158735432768888680120399882384702151467605445407663535984174430480128938313896881639487469658817504506926365338175055478128640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Pour ce qui concerne les nombres réels, les plus petit et plus grand nombres que l’on peut représenter dépendent de la machine et du système d’exploitation utilisé. Cette information nécessite l’utilisation du module sys:

>>> import sys
>>> print(sys.float_info)
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)

Le nombre d’atomes dans l’univers observable est communément estimé autour de \(10^{80}\). La limite imposée par Python ne devrait donc pas être un problème !

Cependant les problèmes de représentation des nombres réels, ainsi que de la précision des calculs qui en découle sont bien connus. La représentation n’est exacte que lorsque le nombre réel est décomposable en une somme de puissances de 2.

La représentation utilisée par la machine est donnée par le module decimal:

>>> from decimal import Decimal
>>> 0.5
0.5
>>> Decimal(0.5) # 0.5 = 2**(-1), la représentation est exacte
Decimal('0.5')

Lorsque le nombre n’est pas décomposable en une somme de puissances de 2, la représentation est approchée:

>>> 0.1
0.1
>>> Decimal(0.1) # 0.1 n'est pas décomposable en une somme de puissances de 2, la représentation est approchée
Decimal('0.1000000000000000055511151231257827021181583404541015625')

Cette représentation approchée des nombres réels doit être connue pour ne pas prendre de décision incorrecte, en particulier lors des tests d’égalité. Observons l’exemple ci dessous:

>>> 0.1 + 0.1 + 0.1
0.30000000000000004

Le résultat de la somme de nombres réels dont la représentation est approchée, est lui même une valeur approchée. En conséquence un test d’égalité naïf échoue:

>>> 0.1 + 0.1 + 0.1 == 0.3
False

Les tests d’égalité entre valeurs réelles doivent utiliser des valeurs arrondies:

>>> round(0.1 + 0.1 + 0.1, 10) == round(0.3, 10)
True

Pour ce qui concerne l’affichage, la console Python affiche 16 chiffres après la virgule:

>>> x = 1.2000000000000001
>>> x
1.2000000000000002
>>> Decimal(x)
Decimal('1.20000000000000017763568394002504646778106689453125')

Au delà, la valeur arrondie est utilisée pour l’affichage, mais la représentation en machine conserve la précision:

>>> x = 1.20000000000000001
>>> x
1.2
>>> Decimal(x)
Decimal('1.1999999999999999555910790149937383830547332763671875')

Les nombres sont des objets

Tout étant objet en Python, chaque classe numérique possède ses propres méthodes. Une façon rapide d’identifier les méthodes disponibles sur un type donné est d’appeler la fonction dir() sur cet objet:

>>> dir(1)
[..., 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

La liste retournée est beaucoup plus longue. Les méthodes préfixées par __ sont des méthodes internes dont l’utilisation sera abordée dans la programmation objet. Elles sont appelées dunder methods (double underscore).

Parmi les autres, certaines ne présentent pas un grand intérêt (pour le moment). Elles permettent cependant de doter les int d’une interface commune avec les float.

Parmi les méthodes intéressantes, on peut connaître la dimension de la représentation binaire d’un int avec la méthode int.bit_length()

>>> (15).bit_length()
4

ou la représentation hexadécimale avec la méthode int.to_bytes():

>>> (15).to_bytes(1, byteorder='big')
b'\x0f'

>>> (15).to_bytes(2, byteorder='big')
b'\x00\x0f'

Ces opérations là étant courantes, elles existent aussi sous la forme de fonctions built in. En Python l’idiome objet.methode() n’est pas gravé dans le marbre. Certaines opérations courantes peuvent être également effectuées avec la syntaxe fonction(paramètres). Les fonctions bin() et hex() en sont un exemple:

>>> bin(15)
'0b1111'

>>> hex(15)
'0xf'

Cependant, en sous main, la plupart des fonctions de Python appellent une méthode de la classe de l’objet passé en paramètre. C’est une fonctionnalité puissante qui permet d’utiliser une même syntaxe, quel que soit l’objet (pour peu que celui ci implémente la méthode). On pourra par exemple obtenir la dimension d’un objet avec la fonction len(), que cet objet soit une chaîne de caractères, une liste, un dictionnaire, …

Les nombres réels ont des méthodes qui leur sont propres:

>>> dir(1.0)
[..., 'as_integer_ratio', 'conjugate', 'fromhex', 'hex', 'imag', 'is_integer', 'real']

Le rôle de la méthode is_integer() est trivial:

>>> (2.0).is_integer()
True

>>> (2.5).is_integer()
False

as_integer_ratio() renvoie deux entiers dont le rapport est la représentation exacte de l’argument, c’est à dire celle donnée par le module decimal:

>>> (2.5).as_integer_ratio()
(5, 2)

>>> 443/37 # représentation approchée
11.972972972972974

>>> Decimal(443/37) # représentation exacte
Decimal('11.9729729729729736931176375946961343288421630859375')

>>> (443/37).as_integer_ratio()
(6740184577449763, 562949953421312)

>>> 6740184577449763/562949953421312 == 443/37
True

>>> 6740184577449763/562949953421312  # représentation approchée
11.972972972972974

>>> Decimal(6740184577449763/562949953421312) # représentation exacte
Decimal('11.9729729729729736931176375946961343288421630859375')

Dans l’immense majorité des cas, une valeur arrondie des nombres réels est suffisante. Sinon, la méthode as_integer_ratio() est utilisée pour manipuler la valeur originale sans perte de précision.

Les nombres complexes ont également des méthodes qui leur sont propres:

>>> dir(1+1j)
[..., 'conjugate', 'imag', 'real']

>>> (1+1j).conjugate()
(1-1j)

>>> (1+1j).real
1.0

>>> (1+1j).imag
1.0

Avertissement

dir() retourne une liste des méthodes et des attributs. La distinction entre les deux n’apparait pas dans la liste retournée. Il faut jeter un oeil à la documentation.

Les opérations arithmétiques

Les opérations arithmétiques sont disponibles dans la console, ce qui permet d’utiliser Python comme une calculette scientifique:

>>> 11 + 3
14

>>> 11 - 3
8

>>> 11 * 3
33

>>> 11 / 3   # division réelle
3.6666666666666665

>>> 11 // 3  # division entière
3

>>> 11 % 3   # modulo
2

>>> divmod(11,3) # division entière + modulo
(3, 2)

>>> 11 ** 3 # exponentiation
1331

La variable spéciale _ contient le dernier résultat:

>>> 5/2
2.5
>>> _
2.5

Les variables

L’instruction d’affectation, comme dans la plupart des autres langages, est le signe =. L’accès à une variable non utilisée produit une erreur:

>>> a = 3
>>> b = 4

Dans l’interpréteur l’évaluation d’une variable retourne sa propre valeur:

>>> a
3

L’instruction ci dessous produit un résultat similaire, mais le mécanisme est différent. print() retourne une chaîne de caractère, et c’est cette chaîne de caractère qui est évaluée:

>>> print(a)
3

Lorsque les instructions seront groupées dans un fichier pour être exécutées en bloc, l’affichage nécessitera l’utilisation de print().

Une tentative d’accès à une variable non initialisée déclenche un message d’erreur dont l’explication est fournie sur la dernière ligne

>>> c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'c' is not defined

L’affectation multiple est possible en Python:

>>> a, b = 3, 4
>>> a
3
>>> b
4

En Python, il n’y a pas de phase de compilation comme en Java ou en C. Le type des variables n’est pas défini de façon statique. Une même variable peut donc faire référence à des objets différents au cours de l’exécution d’un programme:

>>> a = 3
>>> type(a)
<class 'int'>

>>> a = "hello"
>>> type(a)
<class 'str'>

On dit que Python est un langage fortement typé (une variable possède toujours un type objet) et un langage typé dynamiquement (le type affecté à une variable peut changer au cours de l’exécution du programme).

Les booléens

La notion de vérité

On appelle prédicat une expression pouvant seulement prendre les valeurs booléennes True ou False. Les prédicats sont utilisés pour les tests logiques employés par exemples dans les structures de contrôle if et while abordées plus tard.

Par rapport à d’autres langages, Python a une vision élargie de la vérité et considère False les objets suivants:

  • None ;

  • False ;

  • les numériques de valeur nulle : 0, 0.0, 0j ;

  • tout objet vide, c’est à dire de taille nulle.

La fonction bool permet d’illustrer ce comportement:

>>> bool(None)
False

>>> bool(False)
False

>>> bool(0+0j)
False

>>> bool('')
False

Par extension, les autres objets sont True:

  • True ;

  • les numériques de valeur non nulle ;

  • tout objet non vide, c’est à dire de taille non nulle.

Quelques exemples:

>>> bool(True)
True

>>> bool(1)
True

>>> bool('a')
True

Les opérateurs booléens

Les opérateurs classiques and, or et not sont disponibles. Leur comportement est le suivant (pseudo langage):

  • x or y : x is false, then y, else x

  • x and y : x is false, then x, else y

  • not x : x is false, then True, else False

Pour l’opérateur and:

>>> True and False
False

>>> True and True
True

>>> False and True
False

>>> False and False
False

Pour l’opérateur or:

>>> True or False
True
>>> True or True
True
>>> False or True
True
>>> False or False
False

Pour l’opérateur not:

>>> not True
False
>>> not False
True

Les opérateurs booléens ont tous la même priorité. Utiliser les parenthèses pour les groupements:

>>> True and False
False
>>> not True and False
False
>>> (not True) and False
False
>>> not (True and False)
True

L’évaluation des opérateurs logiques se fait de façon paresseuse (lazy). Ci dessous a n’est jamais évaluée:

>>> False and a
False

De même, dans l’expression ci dessous b n’est jamais évaluée:

>>> True or b
True

Au delà des simples opérateurs and, or et not, d’autres opérateurs logiques sont disponibles dans le module operator. Par exemple l’opérateur « ou exclusif » xor():

>>> from operator import xor

>>> xor(0,0)
0

>>> xor(0,1)
1

>>> xor(1,0)
1

>>> xor(1,1)
0

Ce qu’il faut retenir

  • En Python les nombres entiers sont représentés par la classe <class “”>

  • En Python les nombres rééls sont représentés par la classe <class “”>

  • En Python les nombres complexes sont représentés par la classe <class “”>

  • Une opération mathématique nécessite des nombres de même type

  • Python dispose de mécanismes permettant de transformer le type d’un nombre

  • La transformation de type est toujours possible

  • Les nombres entiers sont codés sur 32 bits

  • Les nombres rééls peuvent être représentés avec une précision arbitrairement grande

  • L’égalité entre deux nombres rééls est donnée par l’opérateur ==

  • Un nombre est un objet et dispose donc de méthodes

  • L’opérateur de division entière est /

  • Si a et b sont des entiers, le reste de la division de a par b peut s’obtenir avec divmod()

  • Si a et b sont des entiers, le reste de la division de a par b peut s’obtenir avec %

  • L’opérateur d’exponentiation est **

  • Utiliser une variable permet de stocker un résultat pour réutilisation ultérieure