Version 2025-06
Licence CC–BY–NC–ND
LANGAGE : PYTHON
Christophe Schlick
schlick ಄ u-bordeaux • fr
Comme indiqué dans le chapitre d'introduction, les cellules de code présentes dans un notebook Jupyter, vont être écrites en utilisant, par défaut, la syntaxe du langage Python. En effet, lorsqu'on installe l'environnement Jupyter par la distribution Anaconda, seul le noyau d'exécution du langage Python, appelé ipykernel, est disponible immédiatement. Au lancement du projet Jupyter, trois noyaux différents ont été développés en parallèle, respectivement pour les langages Julia, Python et R, ce qui est à l'origine du nom Ju-Pyt-R. Depuis, des noyaux d'exécution pour près d'une centaine de langages de programmation ont été développés et peuvent être intégrés à l'environnement Jupyter sous forme de plugins.
La documentation complète du langage Python se trouve le site officiel du langage python.org, mais une copie locale est directement disponible dans le menu Help de JupyterLab, sous le titre Python Reference. Ce chapitre n'a pas pour vocation d'être un cours complet sur le langage Python, mais a simplement pour objet de montrer les fonctionnalités les plus utiles dans le cadre d'une utilisation au sein de l'environnement Jupyter. D'autres fonctionnalités seront détaillées dans les chapitres suivants, consacrés aux différents packages de l'écosystème Scientific Python : numpy, pandas, matplotlib, plotly, scipy, sqlite, etc). Et bien évidemment, étant donnée l'extrême popularité du langage depuis une quinzaine d'années, vous trouverez sur le web, des dizaines de sites proposant des formations au langage Python pour tous niveaux. Parmi les sites les plus complets et les plus à jour sur les techniques récentes du développement en Python, figurent notamment les trois suivants :
Si vous maîtrisez déjà (un peu) Python, il peut être utile de télécharger les excellents mémos rédigés par Laurent Pointal, qui regroupent sur deux feuilles A4 recto/verso, la quasi-totalité des éléments de syntaxe du langage. Le premier mémo (appelé mémento Python) se focalise sur les concepts de base, tandis que le second (appelé abrégé Python) inclut des concepts plus avancés (et notamment, tout ce qui concerne la syntaxe liée à la programmation objet) :
Mémento Python de Laurent Pointal ● Abrégé Python de Laurent Pointal
A - Expressions, Instructions, Commentaires
Indépendamment du langage de programmation utilisé pour le noyau, une cellule de code d'un notebook Jupyter va toujours combiner des éléments de trois natures différentes :
- Des expressions qui permettent de générer des données (la plupart du temps en partant d'autres données)
- Des instructions qui permettent d'effectuer des actions sur ces données
- Des commentaires qui permettent de fournir des informations concernant les données et les actions
Ces trois notions sont fondamentales pour comprendre le fonctionnement des cellules de code
1 - Commentaires¶
Un commentaire Python est une séquence de caractères arbitraires, préfixée par le caractère dièse (hash en anglais). Un commentaire peut être placé soit seul sur une ligne, soit derrière une expression ou une instruction :
# Ceci est un commentaire
1 + 2 # Ceci est un commentaire placé derrière une expression
a = 1 + 2 # Ceci est un commentaire placé derrière une instruction
2 - Expressions¶
Une expression Python est une combinaison algébrique mettant en oeuvre des données de natures diverses et des opérateurs prédéfinis par le langage. Les expressions sont réparties dans différentes catégories en fonction de la nature des données qu'elles utilisent.
Dans une cellule de code, une expression est systématiquement évaluée lorsque qu'on valide la cellule, et la valeur obtenue est retournée et affichée juste en dessous de la cellule :
1 + 2
3
L'ajout d'un dièse au début d'une expression la transforme en commentaire, ce qui va bloquer son évaluation :
#1 + 2
L'ajout d'un point-virgule à la fin d'une expression va permettre son évaluation, mais bloquer sa valeur de retour :
1 + 2;
Si on combine plusieurs expressions dans une même cellule, seule est retournée la valeur de la dernière expression :
1 + 2
3 + 4 + 5
6 + 7 + 8 + 9
30
Pour éviter cela, on peut combiner plusieurs expressions sur une même ligne, en les séparant par une virgule :
1 + 2, 3 + 4 + 5, 6 + 7 + 8 + 9
(3, 12, 30)
Par défaut, une expression (de même qu'une instruction, cf. section suivante) doit être contenue toute entière sur une même ligne, car c'est le changement de ligne qui provoque son évaluation par l'interpréteur Python. L'expression suivante provoque ainsi une erreur de type SyntaxError :
# Note : enlever les caractères '#' en préfixe des deux lignes pour provoquer l'erreur de syntaxe
#123456789123456789123456789123456789123456789123456789123456789123456789123456789 +
#987654321987654321987654321987654321987654321987654321987654321987654321987654322
Deux solutions existent pour pouvoir évaluer des expressions longues qui ne tiennent pas sur une seule ligne :
- soit on rajoute le caractère
\(appelée caractère de continuation) qui doit nécessairement être le tout dernier caractère pour chaque ligne à continuer - soit on insère l'expression dans une paire de parenthèses (la parenthèse ouvrante sur la ligne initiale et la parenthèse fermante sur la ligne finale)
Voici un exemple de mise en oeuvre de ces deux solutions :
123456789123456789123456789123456789123456789123456789123456789123456789123456789 + \
987654321987654321987654321987654321987654321987654321987654321987654321987654322
1111111111111111111111111111111111111111111111111111111111111111111111111111111111
(123456789123456789123456789123456789123456789123456789123456789123456789123456789 +
987654321987654321987654321987654321987654321987654321987654321987654321987654322)
1111111111111111111111111111111111111111111111111111111111111111111111111111111111
3 - Instructions¶
Une instruction Python est une combinaison algébrique mettant en oeuvre des expressions arbitraires et des mots-clés prédéfinis par le langage. Comme pour les expressions, les instructions sont réparties dans différentes catégories, en fonction des mots-clés qu'ils utilisent. A la différence d'une expression, une instruction est exécutée et non pas évaluée, et par conséquent, elle ne retourne pas de valeur.
L'instruction de loin la plus fréquente, s'appelle une affectation et consiste à associer un identificateur (séquence arbitraire de lettres et de chiffres) à une expression, afin de pouvoir réutiliser sa valeur par la suite. L'affectation se définit à l'aide du mot-clé = et peut se mettre en oeuvre sous différentes formes :
a = 0 # affectation simple
b, c, d = 1 + 2, 3 + 4 + 5, 6 + 7 + 8 + 9 # affectation multiple
x, y, *z = 1, 2, 3, 4, 5 # affectation multiple avec factorisation (= folding) via le préfixe '*'
Lors d'une affectation, la valeur obtenue par l'évaluation de l'expression se trouvant à droite du signe = est stockée en mémoire, et l'identificateur se trouvant à gauche de ce signe, constitue un label qui permet de désigner cette donnée. Les identificateurs sont stockés dans des structures de données particulières appelées espaces de noms (namespaces en anglais) qui peuvent être vus comme des dictionnaires, permettant d'associer à chaque identificateur, l'adresse dans la mémoire où est stockée le code binaire de la donnée associée à cet identificateur.
Ainsi l'évaluation d'un identificateur consiste simplement à récupérer la valeur stockée en mémoire via l'espace de noms utilisé pour son stockage. Cette opération est très souvent beaucoup plus efficace que l'évaluation de l'expression initiale, donc l'utilisation des identificateurs permet non seulement de gagner en lisibilité, mais également en rapidité :
# Note : enlevez un par un le caractère '#' en préfixe des lignes suivantes pour tester les différents cas
#a # évaluation d'un identificateur unique
#a, b, c, d # évaluation d'identificateurs multiples
#x, y, *z # évaluation avec développement (= unfolding) via le préfixe '*'
#x, y, z # la même chose sans développement
Python est un langage à typage dynamique, ce qui signifie qu'un identificateur ne possède pas de type intrinsèque, mais hérite du type de la donnée qu'on lui a affecté. Autrement dit, si durant le cycle de vie d'un identificateur, on lui affecte successivement des valeurs de natures différentes, son type va évoluer de manière dynamique pour suivre celui de sa valeur courante.
L'analogie la plus simple pour comprendre ce mécanisme, est de considérer un identificateur comme une étiquette qu'on colle sur une donnée pour pouvoir l'identifier et la manipuler de manière symbolique. Cette caractéristique de Python et des autres langages à typage dynamique (Ruby, R, Javascript, PHP, etc) permet une très grande flexibilité, comparé aux langages à typage statique (C, C++, Java, Fortran, etc).
Pour cette même raison, les notions de constantes et de variables, qui sont fondamentales pour les langages à typage statique, sont assez peu pertinentes dans les langages à typage dynamique. En Python, par exemple, il n'y a pas de mot-clé prévu dans la syntaxe du langage pour différencier une constante d'une variable. Il existe une convention d'écriture consistant à mettre les identificateurs des données variables en minuscules et ceux des données constantes en majuscules, mais elle est assez peu respectée (même dans le code écrit par les développeurs du noyau et de la bibliothèque standard du langage).
En pratique, même s'ils ont conscience que ces notions de constantes et de variables ne sont pas pertinentes en Python, la plupart des développeurs en Python continuent de les utiliser, en partie par identification avec ce qui existe pour les langages à typage statique, et en partie parce que le terme identificateur de donnée qui serait certes plus adéquat est quand même extrêmement long à l'écrit et à l'oral.
Une autre instruction très fréquente met en oeuvre la fonction print qui permet d'afficher à l'écran, les expressions fournies en paramètre. Par défaut, le processus d'affichage utilisé par la fonction print insère un espace après chaque argument, et un retour à la ligne après le dernier argument :
print(0, 1 + 2, 3 + 4 + 5, 6 + 7 + 8 + 9)
print(a, b, c, d)
print(x, y, *z, x, y, z)
0 3 12 30 0 3 12 30 1 2 3 4 5 1 2 [3, 4, 5]
Mais ce comportement de la fonction print peut être facilement changé grâce à deux paramètres optionnnels :
sep=...permet de spécifier la chaîne de caractères qui servira de séparateur entre deux argumentsend=...permet de spécifier la chaîne qui servira de terminaison :
print(0, 1 + 2, 3 + 4 + 5, 6 + 7 + 8 + 9, sep=' - ')
print(a, b, c, d, sep='/', end=' --> ')
print(x, y, *z, x, y, z, sep=':', end='\n--- end of data')
0 - 3 - 12 - 30 0/3/12/30 --> 1:2:3:4:5:1:2:[3, 4, 5] --- end of data
Pour connaître le rôle d'une fonction ou obtenir la liste des arguments et options utilisables pour cette fonction, l'interface JupyterLab propose une manière particulièrement efficace : il suffit de placer le curseur d'édition au milieu ou juste devant l'identificateur concerné (par exemple, l'un des print de la cellule précédente) et d'appuyer sur SHIFT + TAB, pour obtenir une fenêtre popup contenant l'ensemble des éléments d'informations sur cet identificateur.
Les mêmes éléments peuvent également être directement intégrés dans le notebook, en entrant l'identificateur pour lequel on souhaite avoir des informations, et ajoutant un point d'interrogation ? en suffixe ou en préfixe :
# la commande print? ou ?print permet d'afficher une aide à l'utilisation
# ATTENTION : le point d'interrogation doit être premier ou dernier caractère de la ligne
print?
Signature: print(*args, sep=' ', end='\n', file=None, flush=False) Docstring: Prints the values to a stream, or to sys.stdout by default. sep string inserted between values, default a space. end string appended after the last value, default a newline. file a file-like object (stream); defaults to the current sys.stdout. flush whether to forcibly flush the stream. Type: builtin_function_or_method
La fonction show est une version alternative de la fonction print et sera fréquemment utilisée pour cette série de tutos, car elle permet de simplifier grandement certaines explications. Comme il ne s'agit pas d'une fonction standard de Python, il faut commencer par signaler l'existence de cette fonction au noyau d'exécution du notebook. Pour cela, on va utiliser la commande import avec la syntaxe suivante :
from SRC.tools import show # import de la fonction 'show' du module 'tools' dans le dossier SRC
A la différence de la fonction print qui n'affiche que la valeur des expressions données en paramètres, la fonction show permet d'afficher à la fois l'expression initiale et la valeur de cette expression, à condition de mettre l'expression entre guillemets. Plusieurs expressions peuvent être évaluées en séquence, en les séparant par des points virgule, chacune de ces expressions s'affichera alors sur une ligne séparée
# chaque caractère ';' génère un retour à la ligne (et donc un ';;' va créer un saut de ligne)
show("0; 1 + 2; 3 + 4 + 5; 6 + 7 + 8 + 9;; a; b; c; d;")
# comme précédemment le préfixe '*' permet d'effectuer une évaluation avec développement (= unfolding)
show("x; y; z; *z; [x, y, *z, x, y, z]; *[x, y, *z, x, y, z]")
0 ➤ 0 1 + 2 ➤ 3 3 + 4 + 5 ➤ 12 6 + 7 + 8 + 9 ➤ 30 a ➤ 0 b ➤ 3 c ➤ 12 d ➤ 30 x ➤ 1 y ➤ 2 z ➤ [3, 4, 5] *z ➤ 3 4 5 [x, y, *z, x, y, z] ➤ [1, 2, 3, 4, 5, 1, 2, [3, 4, 5]] *[x, y, *z, x, y, z] ➤ 1 2 3 4 5 1 2 [3, 4, 5]
B - Types de données atomiques
Les types de données atomiques permettent de stocker les données élémentaires manipulées par un langage de programmation. Ces données atomiques peuvent apparaître sous deux formes différentes dans un code informatique :
- Si la donnée est connue au moment de l'écriture du code, elle va être généralement être écrite de manière littérale en utilisant une convention de notation qui va dépendre du langage de programmation utilisé
- Si la donnée n'est connue qu'au moment de l'exécution du code, elle va être écrite de manière symbolique, habituellement sous la forme d'une expression algébrique qui sera évaluée par le noyau d'exécution
Le noyau Python possède cinq types de données atomiques prédéfinis (on parle également de types natifs ou built-in types), mais la nature extensible du langage permet d'en rajouter de nouveaux, quasiment sans restriction. Cette section va effectuer un tour d'horizon rapide des fonctionnalités disponibles pour ces types atomiques. Pour obtenir des informations plus complètes, il sera judicieux de se reporter au chapitre Types natifs dans le manuel de référence du langage Python.
1 - Vide¶
Le type None correspond à la notion mathématique de l'ensemble vide
- Le vide ne peut contenir que la valeur littérale
None(notez la majuscule initiale) - On ne peut lui appliquer aucun opérateur
a = None
show("a")
a ➤ None
2 - Booléens¶
Le type bool correspond à la notion mathématique de l'ensemble des booléens
- Les booléens ne possèdent que les deux valeurs littérales :
TrueetFalse(notez la majuscule initiale) - Le langage Python propose trois des quatre opérateurs standards de l'algèbre booléenne :
notandor
L'opérateurxor(exclusive or) n'est pas prédéfini mais peut s'obtenir par combinaison des trois autres - Dans beaucoup de situations, l'interpréteur Python va automatiquement convertir une donnée de nature quelconque en valeur booléenne. Pour cela, une règle de conversion très simple est mise en oeuvre :
- Toute donnée atomique nulle ou tout conteneur vide est considéré comme un booléen
False - Toute donnée atomique non nulle ou tout conteneur non vide est considéré comme un booléen
True
- Toute donnée atomique nulle ou tout conteneur vide est considéré comme un booléen
a, b = True, False
show("a; b; not a; not b; a and b; a and a; a or b; b or b")
show("(a and not b) or (b and not a)") # on obtient l'opérateur 'xor' par combinaison algébrique
a ➤ True b ➤ False not a ➤ False not b ➤ True a and b ➤ False a and a ➤ True a or b ➤ True b or b ➤ False (a and not b) or (b and not a) ➤ True
3 - Entiers¶
Le type int correspond à la notion mathématique de l'ensemble ℤ des nombres entiers
- Les valeurs littérales utilisées pour la manipulation et l'affichage des entiers sont définies en base 10
- Néanmoins, l'interpréteur Python va convertir ces valeurs littérales et les stocker en base 2
- La précision utilisée pour le stockage des entiers est dite "infinie" (c'est-à-dire que la quantité de mémoire disponible est le seul facteur limitant pour la taille des entiers manipulables par l'interpréteur Python)
- On peut appliquer aux valeurs de type
int, les sept opérateurs standards de l'arithmétique entière :+(addition)-(soustraction)*(multiplication)/(division)%(modulo)//(quotient)**(exponentiation)
i, j = 654, 321
show("i; j; i + j; i - j; i * j; i / j; i // j; i % j; (i // j) * j + (i % j); i ** j")
i ➤ 654 j ➤ 321 i + j ➤ 975 i - j ➤ 333 i * j ➤ 209934 i / j ➤ 2.0373831775700935 i // j ➤ 2 i % j ➤ 12 (i // j) * j + (i % j) ➤ 654 i ** j ➤ 6316219470564986556862360676247200428481816653513007915435299355589579425024021489731425526784398569774071132939339304833554281926442407085193322959173739533055312342404718189769352843605313729745561287880227553379955497050761254511323315246638186197597553682229518332851427051017895829921027848345712389539897194952767103658454042501202068985192000565782195626523323968187509459568451267829903171120034801012171332166598871422796845344051139437422874010123695379605362748243767939352901829134984183307771195520171092445522050651129666769265623815415179832498746090436757336603265007706999891584371116934834962060005714711941980415168671281048184842491904983538791421069054982526539636421421950736317900662951364708745165391462601976413035094203505451493888605731077381539962830266156703709793302402125223802725335400087249684177308083990816750563337139900498097959128084279812142464472153511670006677504
4 - Réels¶
Le type float correspond à la notion mathématique de l'ensemble ℝ des nombres réels
- Comme pour les entiers, les valeurs littérales utilisées pour la manipulation et l'affichage des nombres réels sont définies en base 10. Attention, la notation anglo-saxonne du point décimal est utilisée pour séparer partie entière et partie fractionnaire, au lieu de la virgule utilisée par la notation latine.
- Les réels sont convertis et stockés en base 2 en utilisant la version "double precision" de la norme de codage IEEE 754, ce qui correspond à 8 octets en mémoire, pour le stockage de chaque nombre réel.
- Ce stockage fournit environ 16 chiffres significatifs pour les calculs, et va limiter le domaine du type
floatà un sous-ensemble fini de ℝ, mais considéré comme suffisant pour la plupart des applications scientifiques :- la plus petite valeur positive représentable correspond environ à $5.1\times10^{-324}$ (idem pour négatif)
- la plus grande valeur positive représentable correspond environ à $1.7\times10^{+308}$ (idem pour négatif)
- Cette limitation du domaine va également engendrer des erreurs d'arrondis lors de la conversion de la base 2 (stockage) vers la base 10 (affichage), comme le montre l'un des exemples ci-dessous
- On peut appliquer aux valeurs de type
float, les cinq opérateurs standard de l'arithmétique réelle :+(addition)-(soustraction)*(multiplication)/(division)**(exponentiation) - Les fonctions mathématiques usuelles sur l'ensemble ℝ (sinus, cosinus, racine carrée, exponentielle, logarithme, ...) ne sont pas directement disponibles dans le noyau du langage, mais peuvent être importées du module
mathde la bibliothèque standard de Python (cf. section F pour un résumé des modules fournis par la bibliothèque standard)
x, y = 12.34, 5.678E-9
show("x; y; x + y; x - y; x * y; x / y; y / x; x ** y; y ** x;; 0.1 + 0.2; round(0.1 + 0.2, 5)")
x ➤ 12.34 y ➤ 5.678e-09 x + y ➤ 12.340000005678 x - y ➤ 12.339999994322 x * y ➤ 7.006652e-08 x / y ➤ 2173300457.907714 y / x ➤ 4.6012965964343597e-10 x ** y ➤ 1.0000000142679397 y ** x ➤ 1.765107215419599e-102 0.1 + 0.2 ➤ 0.30000000000000004 round(0.1 + 0.2, 5) ➤ 0.3
from math import pi, sin, cos, isclose # import des fonctions souhaitées du module 'math'
show("pi; sin(3*pi/4); cos(pi/3); 2*cos(pi/3) == 1; isclose(2*cos(pi/3), 1)")
pi ➤ 3.141592653589793 sin(3*pi/4) ➤ 0.7071067811865476 cos(pi/3) ➤ 0.5000000000000001 2*cos(pi/3) == 1 ➤ False isclose(2*cos(pi/3), 1) ➤ True
5 - Complexes¶
Le type complex correspond à la notion mathématique de l'ensemble ℂ des nombres complexes
- Les complexes sont stockés comme un couple de
float(partie réelle et partie imaginaire) - On peut leur appliquer les cinq opérateurs standard de l'arithmétique complexe :
+(addition)-(soustraction)*(multiplication)/(division)**(exponentiation) - Les parties réelle et imaginaire sont accessibles par la notation pointée :
.real.imag - La bibliothèque standard de Python fournit un module
cmathqui regroupe les fonctions usuelles sur l'ensemble ℂ (cf. section F pour un résumé des modules fournis par la bibliothèque standard)
z, w = 123 + 456j, complex(1.23, 4.56)
show("z; w; z + w; z - w; z * w; z / w; w / z; z ** w; w ** z; z.real; z.imag")
z ➤ (123+456j) w ➤ (1.23+4.56j) z + w ➤ (124.23+460.56j) z - w ➤ (121.77+451.44j) z * w ➤ (-1928.0699999999997+1121.76j) z / w ➤ (100.00000000000003+0j) w / z ➤ (0.009999999999999998+0j) z ** w ➤ (-0.791141274259288-4.952214393440627j) w ** z ➤ (-6.745603727003392e-178+1.0624597261987988e-176j) z.real ➤ 123.0 z.imag ➤ 456.0
from cmath import phase, polar, rect, exp, pi
show("abs(1+1j); phase(1+1j); polar(1+1j); rect(2**0.5,pi/4); exp(0.25j*pi)")
abs(1+1j) ➤ 1.4142135623730951 phase(1+1j) ➤ 0.7853981633974483 polar(1+1j) ➤ (1.4142135623730951, 0.7853981633974483) rect(2**0.5,pi/4) ➤ (1.0000000000000002+1.0000000000000002j) exp(0.25j*pi) ➤ (0.7071067811865476+0.7071067811865476j)
C - Types de données structurées
Les types de données structurées, utilisés par les langages de programmation, servent à regrouper plusieurs données atomiques dans des conteneurs, qui permettront de manipuler ces données, soit de manière individuelle, soit de manière groupée. Tous les conteneurs dans tous les langages de programmation possèdent 2 propriétés fondamentales qui régissent le type de manipulations que l'on peut effectuer sur les données qu'ils contiennent. Plus précisément, la première propriété définit si un conteneur est ordonné ou non-ordonné, et la seconde propriété indique si un conteneur est mutable ou non-mutable.
Pour la première propriété, ce qui différencie un conteneur ordonné d'un conteneur non-ordonné, est l'existence ou l'absence d'un indice numérique qui permet de repérer la position de chacune des données stockées dans ce conteneur. Il faut noter que dans le cas général, cet indice numérique est en fait un multi-indice au sens mathématique, permettant de repérer la position des données dans un espace multidimensionnel (comme c'est le cas pour les éléments d'une matrice, par exemple).
Pour la seconde propriété, ce qui différencie un conteneur mutable d'un conteneur non-mutable est l'autorisation ou l'interdiction de modifier une donnée stockée dans ce conteneur. Les conteneurs mutables sont évidemment ceux dont l'utilisation est la plus commune, puisqu'ils permettent de faire évoluer dynamiquement les données qu'ils contiennent durant l'exécution des algorithmes. Mais les conteneurs non-mutables présentent d'autres avantages spécifiques, tels qu'un stockage mémoire plus compact, un accès aux données plus efficace, et la possibilité de pouvoir utiliser ces conteneurs comme valeurs par défaut pour les arguments des fonctions.
Ces deux propriétés permettent de classer tous les types de données structurées en quatre grandes catégories. Voici ce que cela donne pour les huit conteneurs standards disponibles dans le noyau Python :
- Conteneurs ordonnés, mutables :
list,bytearray - Conteneurs ordonnés, non-mutables :
str,tuple,bytes - Conteneurs non-ordonnés, mutables :
set,dict - Conteneurs non-ordonnés, non-mutables :
frozenset
A l'instar des données atomiques, les données structurées peuvent apparaître sous deux formes différentes dans un code informatique :
- Si les données à inclure dans le conteneur sont connues au moment de l'écriture du code, elles vont être écrites par extension, en utilisant une convention de notation qui va dépendre du langage de programmation utilisé (souvent cela implique des caractères spécifiques pour délimiter les données associées au conteneur)
- Si les données à inclure ne sont connues qu'au moment de l'exécution du code, elles vont être écrites par compréhension, c'est-à-dire en utilisant un mécanisme algorithmique permettant de remplir le conteneur de manière dynamique
Le noyau Python définit huit types de données structurées directement utilisables, mais la nature extensible du langage permet d'en rajouter de nouveaux, quasiment sans restriction (si l'on regroupe l'ensemble des modules d'extension disponibles à ce jour, Python permet d'utiliser plusieurs milliers de types différents pour manipuler des données structurées). Cette section va effectuer un tour d'horizon rapide des fonctionnalités disponibles pour ces types structurés. Pour obtenir des informations plus complètes, il sera judicieux de se reporter au chapitre Types natifs du manuel de référence du langage, ainsi qu'au chapitre Structures de données du tutoriel Python officiel.
1 - Type 'str'¶
Le type str correspond à une séquence/chaîne de caractères de longueur arbitraire. Il s'agit d'un conteneur ordonné et non-mutable :
- La syntaxe du langage Python permet d'utiliser 4 délimiteurs différents pour définir des chaînes de caractères :
- les chaînes courtes (= mono-lignes) peuvent être délimitées par
' ... 'ou" ... " - les chaînes longues (= multi-lignes) peuvent être délimitées par
''' ... '''ou""" ... """
- les chaînes courtes (= mono-lignes) peuvent être délimitées par
- Les caractères d'une chaîne sont définis selon le standard Unicode (nom usuel de la norme ISO/CEI 10646) et stockés en utilisant l'encodage UTF-8 (taille de stockage variable entre 1 ou 4 octets par caractère). Actuellement, la version courante du standard Unicode regroupe environ 155000 caractères différents, sachant que le codage mis en place peut contenir potentiellement $2^{21}$ soit 2097152 caractères au maximum. Depuis sa version initiale publiée en octobre 1991, la norme reste évolutive et connait une révision annuelle ou bisannuelle (la version 16 a été publiée en septembre 2024, la version 17 est planifiée pour 2026)
- Tout caractère Unicode inséré dans une chaîne, peut être :
- soit saisi directement au clavier (pour les caractères usuels)
- soit copié/collé à partir d'un autre document contenant ce caractère
- soit identifié par une séquence
\x..où..représente les 2 chiffres hexadécimaux du caractère, restreint au bloc Unicode 1 (caractères utilisés dans les langues basées sur l'alphabet latin) - soit identifié par une séquence
\u....où....représente les 4 chiffres hexadécimaux du caractère dans la table Unicode complète
- On peut accéder à un caractère donné d'une chaîne en utilisant la syntaxe
[index]et à une tranche de caractères (slice en anglais) en utilisant la syntaxe[start:stop:step] - Le caractère
\(appelé backslash) a un rôle particulier dans les chaînes (on parle de caractère d'échappement) car le caractère qui le suit sera identifié comme un caractère spécial. Ainsi\nreprésente un retour à la ligne,\tune tabulation,\'une quote,\"une double-quote,\ule préfixe du code hexadécimal (4 chiffres) d'un caractère Unicode,\xle préfixe du code hexadécimal (2 chiffres) d'un caractère ISO-LATIN, et enfin\\représente le caractère backslash lui-même - On peut uniquement appliquer deux opérateurs aux chaînes :
+(= concaténation)*(= répétition) - Mais les chaînes possèdent également de nombreuses méthodes internes (cf. table ci-dessous) permettant de manipuler les données qui y sont stockées. Le rôle de ces différentes méthodes sont détaillées dans cette page du manuel de référence du langage Python.
| isprintable | isspace | isascii | islower | isupper | istitle |
| isidentifier | isalnum | isalpha | isnumeric | isdecimal | isdigit |
| lower | upper | capitalize | title | swapcase | casefold |
| strip | lstrip | rstrip | startswith | endswith | translate |
| split | splitlines | lsplit | rsplit | partition | rpartition |
| join | center | ljust | rjust | zfill | format |
| find | rfind | index | rindex | replace | count |
s = 'abcde 12345 \'#!€" çêñåß ♠♣♥♦' # création de chaînes par extension
t = "abcde 12345 '#!€\" \xE7\xEA\xFA\xE5\xDF \u2660\u2663\u2665\u2666" # idem avec les codes hexa
show("s; t") # les deux chaînes sont identiques
s ➤ abcde 12345 '#!€" çêñåß ♠♣♥♦ t ➤ abcde 12345 '#!€" çêúåß ♠♣♥♦
# les fonction 'ord' et 'chr' permettent de passer du caractère au code Unicode et vice versa
show("ord('a'); chr(97); ord('●'); chr(9679)") # attention les valeurs sont en décimal, pas en hexadécimal
ord('a') ➤ 97
chr(97) ➤ a
ord('●') ➤ 9679
chr(9679) ➤ ●
# La lecture des caractères d'une chaîne peut se faire par indice ou par tranche d'indices
show("s[0]; s[-1]; s[:5]; s[-4:]; s[6:17]; s[::3]")
s[0] ➤ a s[-1] ➤ ♦ s[:5] ➤ abcde s[-4:] ➤ ♠♣♥♦ s[6:17] ➤ 12345 '#!€" s[::3] ➤ ad14'€çå♠♦
show("'abc'+'def'; 3*'abc def '") # utilisation des opérateurs de concaténation et de répétition
'abc'+'def' ➤ abcdef 3*'abc def ' ➤ abc def abc def abc def
show("s.upper(); s.center(80, '.'); s.replace('12345', t[-4:])") # utilisation des méthodes internes
s.upper() ➤ ABCDE 12345 '#!€" ÇÊÑÅSS ♠♣♥♦
s.center(80, '.') ➤ ..........................abcde 12345 '#!€" çêñåß ♠♣♥♦..........................
s.replace('12345', t[-4:]) ➤ abcde ♠♣♥♦ '#!€" çêñåß ♠♣♥♦
s = ' abcd\n 1234\n'
t = '''
abcd
1234
'''
show("s#; t#; s.strip()#; t.strip()#") # le suffixe '#' rajoute un retour à la ligne après la flèche
s ➤
abcd
1234
t ➤
abcd
1234
s.strip() ➤
abcd
1234
t.strip() ➤
abcd
1234
En plus des chaînes standards utilisées ci-dessus, le langage Python admet également deux autres syntaxes pour la création des chaînes :
- les chaînes brutes (raw strings ou r-strings) qui s'obtiennent en préfixant le délimiteur de chaîne par le caractère
r, permettent d'annuler le rôle du backslash\comme caractère d'échappement, ce qui s'avère parfois utile, notamment lorsqu'on crée des chaînes correspondant à du code écrit dans un autre langage (par exemple, le langage de description LaTeX qui utilise le caractère backslash en préfixe de chacune de ses commandes) - les chaînes formatées (formatted strings ou f-strings) obtenues en préfixant le délimiteur de chaîne par le caractère
f, permettent de créer facilement des chaînes dynamiques dont le contenu va dépendre des calculs et des traitements en cours, lors de l'exécution d'un programme. Elles permettent également de mieux contrôler l'alignement du contenu (à droite, à gauche ou centré) pour une valeur donnée
s = r"abcde 12345 \t \'#!€\" \n ♠ ♣ ♥ ● \u2660\u2663\u2665\u2666"
show("s") # création d'une chaîne brute (= les '\' ne sont plus des caractères d'échappement)
s ➤ abcde 12345 \t \'#!€\" \n ♠ ♣ ♥ ● \u2660\u2663\u2665\u2666
a, b = 1234, 12.34
s = f"{a} + 2*{b} = {a+2*b}" # création d'une chaîne formatée à partir des identificateurs 'a' et 'b'
t = f"|{a:<10}|{a:^10}|{a:>10}|" # le suffixe ':' permet de contrôler la largeur et l'alignement
show("s; t")
s ➤ 1234 + 2*12.34 = 1258.68 t ➤ |1234 | 1234 | 1234|
2 - Types 'tuple' et 'list'¶
Le type tuple correspond à un conteneur ordonné et non-mutable pouvant stocker une séquence de données de nature arbitraire. Le type list est l'équivalent des tuples, mais en version mutable
- La syntaxe du langage Python utilise deux délimiteurs différents pour définir des tuples ou des listes :
les tuples sont délimités par( ... )et les listes par[ ... ] - Les éléments des tuples/listes définis par extension, sont toujours séparés par des virgules
, - Un tuple vide est défini par une paire de parenthèses
()et une liste vide par une paire de crochets[] - On peut uniquement appliquer deux opérateurs aux tuples/listes :
+(= concaténation)*(= répétition) - Mais les tuples/listes possèdent également différentes méthodes internes permettant de manipuler les données qui y sont stockées. La liste et le rôle de ces méthodes sont détaillées dans cette page du tutoriel officiel du langage Python
s = [1, 2, 3, 4, 5, 6, 7, 8, 9] # création d'une liste par extension
t = (1, 2, 3, 4, 5, 6, 7, 8, 9) # création d'une tuple par extension
show("s; t")
s ➤ [1, 2, 3, 4, 5, 6, 7, 8, 9] t ➤ (1, 2, 3, 4, 5, 6, 7, 8, 9)
# La lecture des éléments d'une liste/tuple peut se faire par indice ou par tranche d'indices
show("s[0]; t[-1]; s[2:5]; t[1:-1]; s[1:8:2]; t[::-1]")
s[0] ➤ 1 t[-1] ➤ 9 s[2:5] ➤ [3, 4, 5] t[1:-1] ➤ (2, 3, 4, 5, 6, 7, 8) s[1:8:2] ➤ [2, 4, 6, 8] t[::-1] ➤ (9, 8, 7, 6, 5, 4, 3, 2, 1)
# La modification des éléments d'une liste peut se faire par indice ou par tranche d'indices
s[0] = 'a'; show("s")
s[-1] = 'z'; show("s")
s[1:8:2] = [-2, -4, -6, -8]; show("s")
s[4:5] = [s[0], s[-1]] * 3; show("s")
s ➤ ['a', 2, 3, 4, 5, 6, 7, 8, 9] s ➤ ['a', 2, 3, 4, 5, 6, 7, 8, 'z'] s ➤ ['a', -2, 3, -4, 5, -6, 7, -8, 'z'] s ➤ ['a', -2, 3, -4, 'a', 'z', 'a', 'z', 'a', 'z', -6, 7, -8, 'z']
# la modification des éléments (item assignment) d'un tuple est interdite
#t[0] = 'a' # enlever le préfixe '#' pour générer le message d'erreur
# Une liste peut contenir tout type de données (y compris d'autres listes)
r = [None, [1, 2, 3], [True, ['abc', '123'], 12.345, 67+89j]]
show("r")
r ➤ [None, [1, 2, 3], [True, ['abc', '123'], 12.345, (67+89j)]]
# Une liste peut même se contenir elle-même (une liste est juste une adresse en mémoire)
r[0] = r # le premier élément de la liste devient la liste elle-même
show("r; r[0]; r[0][0]; r[0][0][0][0][0][0][0][0][0][0]")
r ➤ [[...], [1, 2, 3], [True, ['abc', '123'], 12.345, (67+89j)]] r[0] ➤ [[...], [1, 2, 3], [True, ['abc', '123'], 12.345, (67+89j)]] r[0][0] ➤ [[...], [1, 2, 3], [True, ['abc', '123'], 12.345, (67+89j)]] r[0][0][0][0][0][0][0][0][0][0] ➤ [[...], [1, 2, 3], [True, ['abc', '123'], 12.345, (67+89j)]]
Un processus extrêmement fréquent en Sciences des Données, consiste à convertir des chaînes en listes, et inversement des listes en chaînes. Cela s'effectue très facilement grâce aux méthodes duales str.split(sep) et sep.join(lst), avec tout de même une petite subtilité :
- la méthode
splits'applique à la chaînestrqu'on cherche à découper, et on lui fournit en paramètre le motifsepà détecter pour effectuer le découpage. Le motif de séparation par défaut est défini comme l'union des trois caractères d'espacement, appelés whitespace : espace, tabulation, retour à la ligne - à l'inverse, la méthode
joins'applique au motifseqque l'on veut insérer entre chaque élément, et on lui fournit en paramètre la listelstqui sera fusionnée. Aattention, si les éléments delstne sont pas tous des chaînes de caractères, la méthode renverra une erreur
s = 'toto titi tata tutu'
t = ['AAA', 'BBB', 'CCC']
show("s; s.split(); s.split('titi'); t; ''.join(t); '-|-'.join(t)")
s ➤ toto titi tata tutu
s.split() ➤ ['toto', 'titi', 'tata', 'tutu']
s.split('titi') ➤ ['toto ', ' tata tutu']
t ➤ ['AAA', 'BBB', 'CCC']
''.join(t) ➤ AAABBBCCC
'-|-'.join(t) ➤ AAA-|-BBB-|-CCC
3 - Types '`set' et 'frozenset'¶
Le type set correspond à la notion mathématique d'ensemble telle qu'elle est définie en algèbre (collection d'éléments arbitraires ne pouvant contenir de doublons). Il s'agit donc d'un conteneur non-ordonné et mutable pouvant stocker des données de nature arbitraire. Le type frozenset (ensemble figé) est l'équivalent des ensembles, mais en version non-mutable.
- La syntaxe du langage Python utilise les délimiteurs
{ ... }pour définir des ensembles par extension - Par contre, les ensembles figés ne possèdent pas de délimiteur attitré, il faudra donc appeler le constructeur
frozenseten mettant la série des éléments en paramètre (cf. exemples ci-dessous) - Les éléments des ensembles définis par extension, sont toujours séparés par des virgules
, - Un ensemble vide ou ensemble figé vide est créé en appelant le constructeur correspondant sans paramètre :
set()oufrozenset(). La paire d'accolades{}définit un dictionnaire vide et non en ensemble vide (cf. section suivante) - Les ensembles possèdent les dix opérateurs classiques de la théorie des ensembles :
|(= union)&(= intersection)-(= différence)^(= différence symétrique)
in(= appartenance)not in(= non-appartenance)<(= inclusion gauche stricte)
<=(= inclusion gauche large)>(= inclusion droite stricte)>=(= inclusion droite large) - Mais les ensembles/ensembles figés possèdent également différentes méthodes internes permettant de manipuler les données qui y sont stockées. La liste et le rôle de ces méthodes sont détaillées dans cette page du tutoriel officiel du langage Python
r = {1,2,3} # création d'un ensemble par extension
s = set([1,2,2,4,3,1,4,4,2,5,3]) # création d'un ensemble à partir d'une liste
t = frozenset('abracadabra') # création d'un ensemble figé à partir d'une chaîne
show("r; s; t") # les occurrences multiples sont automatiquement enlevés
r ➤ {1, 2, 3}
s ➤ {1, 2, 3, 4, 5}
t ➤ frozenset({'c', 'r', 'd', 'a', 'b'})
# on peut ajouter ou supprimer des éléments dans un ensemble
r.add(0); show("r") # ajout d'un élément unique
r.add(3); show("r") # l'ajout d'un élément déjà présent, ne change rien
r.discard(0); show("r") # suppression d'un élément unique
r.discard(4); show("r") # la suppression d'un élément absent, ne change rien
s.update((6,7,8)); show("s") # ajout de plusieurs éléments à la fois
s.difference_update((6,7,8)); show("s") # suppression de plusieurs éléments
r ➤ {0, 1, 2, 3}
r ➤ {0, 1, 2, 3}
r ➤ {1, 2, 3}
r ➤ {1, 2, 3}
s ➤ {1, 2, 3, 4, 5, 6, 7, 8}
s ➤ {1, 2, 3, 4, 5}
# l'ajout ou la suppression d'éléments dans un ensemble gelé sont interdits
#t.add('e') # enlever le préfixe '#' pour générer le message d'erreur
#t.discard('r') # enlever le préfixe '#' pour générer le message d'erreur
# les opérateurs 'union', 'intersection', 'différence' et 'différence symétrique'
show("r|s; r|t; r&s; r&t; s-r; r-s; r^t; t^r")
r|s ➤ {1, 2, 3, 4, 5}
r|t ➤ {1, 2, 3, 'c', 'r', 'd', 'a', 'b'}
r&s ➤ {1, 2, 3}
r&t ➤ set()
s-r ➤ {4, 5}
r-s ➤ set()
r^t ➤ {1, 'c', 2, 'd', 3, 'r', 'a', 'b'}
t^r ➤ frozenset({1, 2, 3, 'c', 'r', 'd', 'a', 'b'})
# les opérateurs 'appartenance' et 'inclusion'
show("1 in r; 4 not in r; r < s; s < s; s <= s; t > set('abc')")
1 in r ➤ True
4 not in r ➤ True
r < s ➤ True
s < s ➤ False
s <= s ➤ True
t > set('abc') ➤ True
4 - Type 'dict'¶
Le type dict correspond à la notion mathématique d'application (mapping en anglais) telle qu'elle est définie en algèbre (relation entre deux ensembles où chaque élément de l'ensemble de départ possède une seule image dans l'ensemble d'arrivée). Dans le langage Python, cela correspond à un conteneur non-ordonné et mutable pouvant stocker des données de nature arbitraire.
Note : Le langage Python ne propose pas d'équivalent non-mutable d'un dictionnaire, car en pratique, l'utilisation d'un dictionnaire figé est extrêmement rare, et peut généralement être remplacé par un tuple de couples (clé,valeur)
- La syntaxe du langage Python utilise les mêmes délimiteurs
{ ... }pour créer des ensembles et des dictionnaires par extension. Malgré cela, il n'y a pas de confusion possibles, puisque un ensemble sera défini en fournissant une série de valeurs séparées par des virgules, alors qu'un dictionnaire sera défini par un ensemble de couplesclé:valeur - Une contrainte importante d'un dictionnaire est que ses clés doivent obligatoirement être des données non-mutables, alors que les valeurs associées peuvent être mutables ou non
- Le seul opérateur pouvant être appliqué à un dictionnaire est la fusion de deux dictionnaires, symbolisé par l'opérateur
|(= fusion) - Mais les dictionnaires possèdent également différentes méthodes internes permettant de manipuler les données qui y sont stockées. La liste et le rôle de ces méthodes sont détaillées dans cette page du tutoriel officiel du langage Python
s = {'toto':1, 'titi':2, 'tutu':3} # création d'un dictionnaire par extension
t = dict(toto=4, tata=5, tete=6) # version alternative (valable pour des clés de type 'identificateur')
show("s; t; s|t; {**s, **t}") # dans une fusion, les valeurs du dictionnaire de droite sont prioritaires
# note : l'opérateur de fusion de dictionnaires '|' n'a été introduit que depuis la version 3.9 de Python
# auparavant, on utilisait la syntaxe alternative {**s, **t} nettement moins lisible
s ➤ {'toto': 1, 'titi': 2, 'tutu': 3}
t ➤ {'toto': 4, 'tata': 5, 'tete': 6}
s|t ➤ {'toto': 4, 'titi': 2, 'tutu': 3, 'tata': 5, 'tete': 6}
{**s, **t} ➤ {'toto': 4, 'titi': 2, 'tutu': 3, 'tata': 5, 'tete': 6}
keys, vals = [(1,2), (3,4), (5,6)], ['toto', 'titi', 'tutu'] # création de listes de clés et de valeurs
kv = dict(zip(keys, vals)) # création par entrelacement des clés et des valeurs
vk = dict(zip(vals, keys)) # idem en inversant les listes (possible si les éléments sont non-mutables)
show("kv; vk")
kv ➤ {(1, 2): 'toto', (3, 4): 'titi', (5, 6): 'tutu'}
vk ➤ {'toto': (1, 2), 'titi': (3, 4), 'tutu': (5, 6)}
# La lecture des éléments d'un dictionnaire peut se faire uniquement par clé
# mais on peut récupérer la liste complète des clés, des valeurs ou des couples (clé,valeur)
show("s['toto']; s['tutu']; list(s.keys()); list(s.values()); list(s.items())")
s['toto'] ➤ 1
s['tutu'] ➤ 3
list(s.keys()) ➤ ['toto', 'titi', 'tutu']
list(s.values()) ➤ [1, 2, 3]
list(s.items()) ➤ [('toto', 1), ('titi', 2), ('tutu', 3)]
# La modification des éléments d'une liste peut se faire uniquement par clé
s['tutu'] = -1; show("s") # modification d'un élément
s['toto'], s['titi'] = s['titi'], s['toto']; show("s") # permutation de 2 éléments
s['tata'] = -2; show("s") # insertion d'un nouveau couple (clé,valeur)
s ➤ {'toto': 1, 'titi': 2, 'tutu': -1}
s ➤ {'toto': 2, 'titi': 1, 'tutu': -1}
s ➤ {'toto': 2, 'titi': 1, 'tutu': -1, 'tata': -2}
# Un dictionnaire peut contenir tout type de données (y compris d'autres dictionnaires)
r = dict(a='XYZ', b=dict(i=0,j=1), c=[(1.2,3.4), 'ABCDE'])
show("r")
r ➤ {'a': 'XYZ', 'b': {'i': 0, 'j': 1}, 'c': [(1.2, 3.4), 'ABCDE']}
# Un dictionnaire peut même se contenir lui-même (un dictionnaire est juste une adresse en mémoire)
r['z'] = r # on associe le dictionnaire complet à sa propre clé 'z'
show("r; r['z']; r['z']['z']; r['z']['z']['z']['z']['z']")
r ➤ {'a': 'XYZ', 'b': {'i': 0, 'j': 1}, 'c': [(1.2, 3.4), 'ABCDE'], 'z': {...}}
r['z'] ➤ {'a': 'XYZ', 'b': {'i': 0, 'j': 1}, 'c': [(1.2, 3.4), 'ABCDE'], 'z': {...}}
r['z']['z'] ➤ {'a': 'XYZ', 'b': {'i': 0, 'j': 1}, 'c': [(1.2, 3.4), 'ABCDE'], 'z': {...}}
r['z']['z']['z']['z']['z'] ➤ {'a': 'XYZ', 'b': {'i': 0, 'j': 1}, 'c': [(1.2, 3.4), 'ABCDE'], 'z': {...}}
5 - Types 'bytearray' et 'bytes'¶
Le type bytes correspond à un conteneur ordonné et non-mutable pouvant stocker une séquence d'octets (valeurs entières entre 0 et 255) de longueur arbitraire. Le type bytearray est l'équivalent des bytes, mais en version mutable
- La syntaxe du langage Python ne fournit pas de délimiteur spécifique pour créer des séquences d'octets. La manière usuelle pour initialiser une telle donnée est d'appeler le constructeur
bytesoubytearray, en passant en argument une liste/tuple contenant des nombres entiers (qui doivent tous être compris entre 0 et 255) - A l'inverse, afin de compacter l'affichage d'une séquence d'octets, l'interpréteur Python va transformer les octets en séquence de caractères selon le code ASCII : les valeurs comprises entre 32 et 126 (dits caractères imprimables) sont directement affichés en tant que caractère, et les 161 valeurs restantes sont affichées en hexadécimal avec le préfixe
\x. Pour éviter de confondre l'affichage d'une séquencebytesavec une chaîne de caractères standard, on rajoute un préfixebdevant le délimiteur (cf. exemple ci-dessous) - En pratique, ces deux structures de données sont utilisées assez rarement dans les codes Python. Leur seule utilité concrète se manifeste lors des manipulations de données brutes stockées dans un fichier binaire ou dans un flux de données envoyés par un périphérique d'acquisition, mais cela reste des applications assez particulières
# création d'une séauence de type 'bytes' (= séquence d'octets non mutable)
b = bytes([97, 98, 99, 100, 32, 49, 50, 51, 52, 32, 0, 1, 254, 255])
show("b; list(b); b[-1]; b[:4]")
b ➤ b'abcd 1234 \x00\x01\xfe\xff' list(b) ➤ [97, 98, 99, 100, 32, 49, 50, 51, 52, 32, 0, 1, 254, 255] b[-1] ➤ 255 b[:4] ➤ b'abcd'
# création d'une séquence de type 'bytearray' (= séquence d'octets mutable)
a = bytearray([97, 98, 99, 100, 32, 49, 50, 51, 52, 32, 0, 1, 254, 255])
show("a; list(a); a[-1]; a[:4]")
a[0:4] = [65, 66, 67, 68] # on modifie la valeur des 4 premiers octets
show("a; list(a); a[-1]; a[:4]")
a ➤ bytearray(b'abcd 1234 \x00\x01\xfe\xff') list(a) ➤ [97, 98, 99, 100, 32, 49, 50, 51, 52, 32, 0, 1, 254, 255] a[-1] ➤ 255 a[:4] ➤ bytearray(b'abcd') a ➤ bytearray(b'ABCD 1234 \x00\x01\xfe\xff') list(a) ➤ [65, 66, 67, 68, 32, 49, 50, 51, 52, 32, 0, 1, 254, 255] a[-1] ➤ 255 a[:4] ➤ bytearray(b'ABCD')
D - Contrôle du flux d'exécution
Tous les langages de programmation possèdent des mécanismes permettant de contrôler l'ordre dans lequel les instructions d'un programme sont exécutées (on parle de flux d'exécution). Ces mécanismes se répartissent en trois grandes catégories :
- les tests qui permettent d'exécuter un bloc d'instructions uniquement lorsqu'une certaine expression booléenne est vérifiée
- les répétitions qui permettent de répéter un bloc d'instruction tant qu'une certaine expression booléenne est vérifiée
- les procédures qui permettent de transformer un bloc d'instructions en une entité autonome, qui récupère des données en entrée, effectue un certain traitement, et renvoie d'autres données en sortie
1 - Tests¶
Jusqu'à la version 3.10, le langage Python ne possèdait qu'une seule instruction de test (définie par le mot-clé if ) qui se décline en trois variantes :
Choix simple :
if <test>: <block>
<block>est exécuté uniquement lorsque la valeur de l'expression<test>est égale àTrueChoix binaire :
if <test>: <block1> else: <block0>
<block1>(resp.<block0>) est exécuté lorsque la valeur de l'expression<test>vautTrue(resp.False)Choix multiple :
if <test1>: <block1> elif <test2>: <block2> ... elif <testN>: <blockN> else: <block0>
<test1>,<test2>...<testN>sont évalués dans l'ordre, et le bloc associé à la première valeurTrueobtenue, est exécuté. Si tous les tests donnent une valeurFalse, on exécute<block0>
A titre d'exemple, on va réaliser une série de tests en séquence permettant de détecter le signe et la parité d'un nombre entier, en identifiant les cinq situations possibles : négatif impair / négatif pair / nul / positif impair / positif pair.
La première version va permettre d'obtenir le résultat en utilisant une série de tests imbriqués :
a = 5 # changer la valeur de 'a' pour voir la modification du résultat affiché
if a < 0:
if a % 2: print(f"{a} est négatif impair") # l'expression 'a % 2' est équivalent à 'a % 2 != 0'
else: print(f"{a} est négatif pair")
elif a > 0:
if a % 2: print(f"{a} est positif impair")
else: print(f"{a} est positif pair")
else: print(f"{a} est nul")
5 est positif impair
La seconde version va utiliser l'opérateur d'évaluation conditionnelle, de la forme <val1> if <test> else <val0>, ce qui permet d'éviter les deux tests imbriqués :
a = 5 # changer la valeur de 'a' pour voir la modification du résultat affiché
if a < 0: print(f"{a} est négatif {'impair' if a % 2 else 'pair'}")
elif a > 0: print(f"{a} est positif {'impair' if a % 2 else 'pair'}")
else: print(f"{a} est nul")
5 est positif impair
La troisième version va encore simplifier le code, en réutilisant cet opérateur d'évaluation conditionnelle :
a = 0 # changer la valeur de 'a' pour voir la modification du résultat affiché
if a: print(f"{a} est {'positif' if a > 0 else 'négatif'} {'impair' if a % 2 else 'pair'}")
else: print(f"{a} est nul")
0 est nul
On peut encore compacter le code, en utilisant une troisième fois cet opérateur d'évaluation conditionnelle, mais en contre-partie, la lisibilité est clairement impactée :
a = 5 # changer la valeur de 'a' pour voir la modification du résultat affiché
print(f"{a} est {('posi' if a>0 else 'néga')+'tif '+('im' if a % 2 else '')+'pair' if a else 'nul'}")
5 est positif impair
Pour ce genre de situation, la bonne manière de procéder consiste à bâtir un conteneur qui va stocker "l'espace des possibles" (toutes les valeurs susceptibles de se produire) et de créer un index numérique qui va piocher la bonne valeur en fonction de la situation courante. Avec notre exemple, cela donne :
a = 5 # changer la valeur de 'a' pour voir la modification du résultat affiché
results = "négatif pair : négatif impair : positif pair : positif impair : nul".split(' : ') # 5 termes
print(f"{a} est {results[(0 if a < 0 else 2 if a > 0 else 4) + a % 2]}") # on pioche le bon résultat
5 est positif impair
La version 3.10 de Python a introduit une nouvelle instruction de test, définie par la paire de mots-clés match et case, qui permet de simplifier la syntaxe de certains tests à choix multiple. Voici un exemple d'utilisation assez basique de cette instruction (il faut noter que ce cas de figure aurait pu être traité plus efficacement en utilisant un dictionnaire) :
time = 'teatime' # changer la valeur de 'time' pour voir la modification du résultat affiché
match time:
case 'morning'|'dawn'|'sunrise': action = 'breakfast'
case 'noon'|'midday' : action = 'lunch'
case 'afterlunch': action = 'dessert'
case 'afternoon' : action = 'a snack'
case 'teatime'|'coffeetime' : action = f"a cup of {time[:-4]}"
case 'afterwork' : action = 'a drink'
case 'evening'|'sunset' : action = 'dinner'
case 'partytime' : action = 'fun'
case 'night' : action = 'a good sleep'
case _: action = 'a break' # default case
print(f"It's {time}, let us have {action}")
It's teatime, let us have a cup of tea
Une utilisation plus intéressante de l'intruction match est de s'en servir pour ajuster le traitement à effectuer en fonction de la nature des données. Par exemple, choisir entre une division entière ou une division réelle en fonction du type de données associé aux deux opérandes :
a, b = 5, 3 # changer les valeurs de 'a' et 'b' pour voir la modification du résultat affiché
match (a, b):
case (int(), int()) : out = a // b # use floor division when both operands are integers
case (_, float()) | (float(), _) : out = a / b # use real division with at least one is a float
case _ : out = None # no result when both operands are neither integers nor floats
print(f"{a} / {b} = {out}")
5 / 3 = 1
2 - Boucles et itérateurs¶
Le langage Python possède deux instructions de répétition (définies par les mots-clés while et for ) qui se déclinent en quatre variantes :
Boucle conditionnelle :
while <test>: <block>
<block>est exécuté de manière répétée, tant que la valeur de l'expression<test>est égale àTrue. Pour éviter une boucle infinie, il est donc impératif que<block>modifie au moins une des variables utilisées dans l'expression<test>Boucle conditionnelle avec finalisation :
while <test>: <block1> else: <block0>
<block1>est exécuté de manière répétée, tant que la valeur de l'expression<test>est égale àTrue, puis à la fin de la boucle,<block0>est exécuté, sauf s'il y a eu interruption de boucle avec l'instructionbreakItérateur de conteneur :
for <var> in <container>: <bloc>
La variable<var>va parcourir, en séquence, toutes les données stockées dans<conteneur>et exécuter<bloc>pour chacune de ces données. Le contenu de la variable<var>est ainsi automatiquement modifié à chaque itérationItérateur de conteneur avec finalisation :
for <var> in <container>: <bloc1> else: <block0>
Le bloc de finalisation<block0>est exécuté, sauf s'il y a eu interruption de boucle avec l'instructionbreak
A titre d'exemple, on va utiliser l'instruction while pour calculer la somme des $n$ premiers entiers :
n, p, out = 10, 1, 0 # initialisation du nombre d'itérations, du compteur de boucle et du résultat
while p <= n: # boucle conditionnelle
p, out = p + 1, out + p # mise à jour du compteur et du résultat
print(f"1 + 2 + ... + {n} = {out}") # affichage du résultat après la boucle
1 + 2 + ... + 10 = 55
Dans ce cas de figure, mettre l'instruction print dans le bloc de finalisation ne change rien :
n, p, out = 10, 1, 0 # initialisation du nombre d'itérations, du compteur de boucle et du résultat
while p <= n: # boucle conditionnelle
p, out = p + 1, out + p # mise à jour du compteur et du résultat
else: print(f"1 + 2 + ... + {n} = {out}") # affichage du résultat dans un bloc de finalisation
1 + 2 + ... + 10 = 55
L'inconvénient de la syntaxe de base de l'instruction while est que le test est toujours évalué avant d'exécuter le bloc associé. Parfois, il peut être utile d'effectuer une partie du traitement avant le test et une autre partie après le test. Ce comportement peut s'obtenir en utilisant une boucle pseudo-infinie, c'est-à-dire une boucle infinie while True:, accompagnée d'une rupture conditionnelle via l'instruction if <test>: break :
n, p, out = 10, 1, 0 # initialisation du nombre d'itérations, du compteur de boucle et du résultat
while True: # boucle pseudo-infinie (l'expression 'True' est toujours vrai, par définition)
if p > n: break # rupture de boucle (!!! le test est inversé par rapport au 'while' standard !!!)
p, out = p + 1, out + p # mise à jour du compteur et du résultat
print(f"1 + 2 + ... + {n} = {out}") # affichage du résultat après la boucle
1 + 2 + ... + 10 = 55
Attention, avec une boucle pseudo-infinie, on ne peut plus utiliser la syntaxe avec else puisque le bloc de finalisation ne sera jamais exécuté (sortie de boucle systématique par break) :
n, p, out = 10, 1, 0 # initialisation du nombre d'itérations, du compteur de boucle et du résultat
while True: # boucle pseudo-infinie
if p > n: break # rupture de boucle lorsque le nombre d'itérations est atteint
p, out = p + 1, out + p # mise à jour du compteur et du résultat
else: print(f"1 + 2 + ... + {n} = {out}") # ce bloc n'est jamais exécuté, donc pas d'affichage
Le langage Python possède une seconde instruction de rupture, nommée continue, qui n'interrompt pas l'intégralité de la boucle, mais uniquement la fin du bloc pour l'itération en cours, ce qui permet de "sauter" certaines instructions de la boucle, dont le résultat est soit non nécessaire, soit non souhaitable (susceptible de produire une erreur)
Pour illustrer l'utilisation de l'instruction continue, nous allons modifier notre boucle pour calculer la somme des $n$ premiers entiers, mais en excluant les multiples de trois. Une première implémentation, trop naïve, va générer une boucle infinie, car continue est exécuté avant la modification du compteur de boucle qui ne pourra plus évoluer dès que l'on aura rencontré le premier multiple de trois. Lorsqu'une telle boucle infinie se produit dans un notebook Jupyter, l'utilisateur a comme seule indication, le mot Busy qui reste affiché dans la ligne d'état en bas de la fenêtre. Dans ce cas, l'utilisateur devra interrompre manuellement le noyau d'exécution, en cliquant sur l'icône "carré noir" sur la barre d'outils placée au-dessus du notebook, afin de pouvoir reprendre la main :
# enlever le commentaire de la troisième ligne de code pour créer la boucle infinie
n, p, out = 10, 1, 0 # initialisation du nombre d'itérations, du compteur de boucles et du résultat
while p <= n: # boucle conditionnelle
#if p % 3 == 0: continue # on saute les multiples de 3 (!!! BOUCLE INFINIE !!!)
p, out = p + 1, out + p # mise à jour du compteur et du résultat
print(f"1 + 2 + ... + {n} = {out}") # cette instruction n'est jamais exécuté car boucle infinie
1 + 2 + ... + 10 = 55
Pour éviter cette boucle infinie, il faut modifier l'implémentation, en prenant l'une des deux options possibles :
# soit on gère les cas particuliers en pré-traitement :
# on vérifie que le nombre n'est pas un multiple de trois avant de l'ajouter à la somme
# note : cette option oblige à modifier la valeur initiale du compteur et celle du test de boucle
n, p, out = 10, 0, 0 # initialisation (!!! la valeur initiale du compteur de boucles est modifiée !!!)
while p < n: # boucle conditionnelle (!!! le test du compteur de boucles est également modifié !!!)
p += 1 # mise à jour du compteur de boucles
if p % 3 == 0: continue # on saute les multiples de 3
out += p # mise à jour du résultat
print(f"1 + 2 + ... + {n} = {out}") # affichage du résultat après la boucle
1 + 2 + ... + 10 = 37
# soit on gère les cas particuliers en post-traitement :
# on effectue la somme de tous les nombres y compris les multiples de trois
# puis on corrige cette somme en retranchant les multiples de trois
# note : cette option ne fonctionne que si les cas particuliers ne génèrent pas d'erreurs
n, p, out = 10, 1, 0 # initialisation du nombre d'itérations, du compteur de boucle et du résultat
while p <= n: # boucle conditionnelle
p, out = p + 1, out + p # mise à jour du compteur et du résultat
if p % 3 == 0: out -= p # on corrige le résultat en retranchant les multiples de trois
print(f"1 + 2 + ... + {n} = {out}") # affichage du résultat après la boucle
1 + 2 + ... + 10 = 37
L'instruction for est la seconde instruction disponible en Python pour effectuer des boucles. Elle permet de parcourir itérativement les différents éléments d'un conteneur qu'il soit ordonné ou non, mutable ou non. Néanmoins, la mise en oeuvre peut différer légèrement selon la nature du conteneur :
r = [1, 2, 3, 4, 5, 6, 7, 8, 9] # création d'une liste d'entiers par extension
for n in r: print(f"{n} x {n} = {n*n}") # affichage des carrés pour les valeurs stockées dans la liste
1 x 1 = 1 2 x 2 = 4 3 x 3 = 9 4 x 4 = 16 5 x 5 = 25 6 x 6 = 36 7 x 7 = 49 8 x 8 = 64 9 x 9 = 81
s = 'AAA BBB CCC DDD EEE FFF'.split() # création d'une liste à partir d'une chaîne
for index, value in enumerate(s): print(f"index = {index} ● value = {value}") # énumération des éléments
index = 0 ● value = AAA index = 1 ● value = BBB index = 2 ● value = CCC index = 3 ● value = DDD index = 4 ● value = EEE index = 5 ● value = FFF
# Deux options sont utilisables pour parcourir un dictionnaire via une boucle 'for'
t = {'toto':1, 'titi':2, 'tutu':3} # création d'un dictionnaire par extension
# Première possibilité : parcours des clés et récupération de la valeur associée
for key in t: print(f"key = {key} ● value = {t[key]}")
# Seconde possibilité : parcours des couples (clé, valeur)
for key, value in t.items(): print(f"key = {key} ● value = {value}")
key = toto ● value = 1 key = titi ● value = 2 key = tutu ● value = 3 key = toto ● value = 1 key = titi ● value = 2 key = tutu ● value = 3
En pratique, l'utilisation la plus fréquente des boucles for met en oeuvre l'itérateur range(start,stop,step) qui énumère les valeurs entières entre la valeur start (incluse) et la valeur stop (exclue) en utilisant un incrément de step :
r = range(10) # itérateur de 0 à 9 par pas de 1 (intervalle TOUJOURS fermé à gauche, ouvert à droite)
show("r") # affichage de l'itérateur tel qu'il est défini de manière interne
show("list(r)") # conversion de l'itérateur en liste
r ➤ range(0, 10) list(r) ➤ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for n in r: print(n, end=' ') # boucle de parcours sur les éléments de l'itérateur
print() # on passe à la ligne pour l'affichage suivant
for n in range(10): print(n, end=' ') # idem sans stocker l'itérateur dans une variable
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
s = range(0, 51, 5) # itérateur de 0 à 50 par pas de 5
t = range(50, -1, -5) # itérateur de 50 à 0 par pas de -5
show("s; list(s); t; list(t)")
s ➤ range(0, 51, 5) list(s) ➤ [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50] t ➤ range(50, -1, -5) list(t) ➤ [50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]
Le langage Python possède une deuxième syntaxe pour la mise en oeuvre de l'instruction for, dans laquelle la boucle est placée à l'intérieur des délimiteurs d'un conteneur, ce qui permet de créer les éléments de ce conteneur à la volée. Ce mécanisme s'appelle une construction par compréhension et est utilisé de manière extrêmement fréquente pour la construction dynamique de listes, de tuples et de dictionnaires :
# les itérateurs permettent également de créer des listes, des tuples ou des dictionnaires à la volée
r = [n*n for n in range(21)] # création d'une liste par compréhension (carrés des nombres de 0 à 20)
r = list(n*n for n in range(21)) # syntaxe alternative pour les listes par compréhension
s = (n*n for n in range(21)) # !!! ATTENTION : création d'un générateur (cf. paragraphe suivant) !!!
s = tuple(n*n for n in range(21)) # création d'une tuple par compréhension (seule syntaxe possible)
t = {n : n*n for n in range(11)} # création d'un dictionnaire par compréhension (associant 'n' à son carré)
t = dict((n, n*n) for n in range(11)) # syntaxe alternative pour les dictionnaires par compréhension
show("r; s; t")
r ➤ [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400]
s ➤ (0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400)
t ➤ {0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81, 10: 100}
3 - Fonctions, générateurs et expressions lambda¶
De la même manière que le mot-clé = permet d'associer un identificateur avec une valeur stockée en mémoire, par un processus nommé affectation de donnée, le mot-clé def permet d'associer un identificateur à une séquence d'instructions, par un processus nommé définition de fonction. Concrètement, les deux processus sont extrêmement similaires : dans les deux cas, l'interpréteur Python va associer un identificateur avec une adresse en mémoire où est stockée une séquence binaire d'une taille variable. La seule différence est que dans le cas de l'affectation, cette séquence binaire représente une donnée, alors que dans le cas de la définition, elle représente une séquence d'instructions à effectuer par l'interpréteur.
Dans quasiment tous les langages de programmation, les fonctions représentent la structure de base permettant de décomposer l'ensemble des traitements réalisés par un programme, en des groupes d'instructions correspondant à des tâches spécifiques. Cette universalité provient vraisemblablement du fonctionnement même de notre système cognitif : l'être humain a l'habitude, dans sa vie quotidienne, de décomposer une tâche complexe en une séquence de sous-tâches plus simples à réaliser. Par conséquent, la décomposition d'un code informatique complexe en un ensemble de fonctions simples est vite apparue comme une approche efficace, dès l'invention des premiers langages de programmation.
Le terme de fonction pour les langages de programmation a été choisi par analogie avec la notion de fonction) en mathématiques, avec laquelle elles partagent une certaine similitude : elles prennent un ensemble de données en entrée, effectuent un certain calcul (ou plus globalement, un certain traitement de ces données) et retourne un autre ensemble de données en sortie. Mais en l'occurrence, les fonctions des langages de programmation doivent plutôt être considérées comme une généralisation des fonctions mathématiques, car les éléments syntaxiques tels que les tests, les itérateurs ou les instructions de ruptures permettent de créer des traitements qui ne seraient pas possibles si on se cantonnait à la définition des fonctions purement mathématiques.
De nos jours, le travail d'un développeur logiciel, notamment dans le domaine des Sciences des Données, consiste, dans sa très grande majorité, à assembler les appels à des fonctions de traitement écrites par quelqu'un d'autre, plutôt qu'à écrire ses propres fonctions de traitement. La syntaxe du langage Python permet de mélanger sans aucune limitation, des fonctions externes (écrites à l'extérieur du code courant, le plus souvent par un autre développeur) et des fonctions internes (définies directement dans le code courant). Voici la syntaxe à utiliser pour ces différentes possibilités :
from math import factorial # import d'une fonction externe (se trouvant dans le module standard 'math')
# tester SHIFT+TAB en plaçant le curseur à l'intérieur du mot 'factorial' ci-dessus
for n in range(10): print(f"{n}! = {factorial(n)}")
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880
def facto(n): # définition d'une fonction interne
out = 1
for loop in range(1, n+1): out *= loop
return out # le mot-clé 'return' permet de spécifier les données à renvoyer par la fonction
Pour mieux comprendre, il peut être utile de tester l'exécution incrémentale de la fonction sur PyTutor
for n in range(10): print(f"{n}! = {facto(n)}")
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880
Le langage Python propose également une alternative à la notion de fonction, appelée générateur. Les générateurs peuvent être vus comme des fonctions qui mémorisent leur configuration d'un appel à l'autre; ainsi à chaque fois qu'on appelle un générateur, son exécution ne va pas redémarre à la première ligne de son bloc (comme le ferait une fonction) mais juste derrière l'instruction sur laquelle le générateur s'était arrêté à l'appel précédent.
La syntaxe pour définir une fonction ou un générateur est quasiment équivalente : la seule chose qui change est qu'une fonction utilise le mot-clé return pour renvoyer des données, alors qu'un générateur va utiliser le mot-clé yield.
def factos(n): # definition d'un générateur (pas de 'return' mais un 'yield')
out = 1;
for loop in range(1, n+1):
yield out; out *= loop # à chaque itération, on renvoie la valeur courante de 'out'
Pour mieux comprendre, il peut être utile de tester la fonction sur PyTutor
factos(10) # l'appel au générateur ne renvoie pas de données, mais crée un objet de type 'generator'
<generator object factos at 0x0000020095967060>
Pour récupérer les données produites par un générateur, il existe globalement quatre possibilités :
# option 1 : on passe le résultat à une fonction en utilisant le préfixe '*' (= unfolding)
print(*factos(10))
1 1 2 6 24 120 720 5040 40320 362880
# option 2 : on convertit le résultat directement vers un conteneur (list, tuple)
list(factos(10))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
# option 3 : on crée une boucle pour récupérer les données une par une
for n,f in enumerate(factos(10)): print(f"{n}! = {f}")
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880
# option 4 : on crée une liste ou un tuple par compréhension
[f"{n}! = {f}" for n,f in enumerate(factos(10))]
['0! = 1', '1! = 1', '2! = 2', '3! = 6', '4! = 24', '5! = 120', '6! = 720', '7! = 5040', '8! = 40320', '9! = 362880']
Lorsque les traitements effectués par une fonction se limitent à la simple évaluation d'une expression, définissant une valeur de retour calculée en fonction des valeurs des paramètres d'entrées, on peut utiliser une forme syntaxique plus concise et plus efficace, appelée expression lambda, en utilisant le mot-clé lambda à la place du couple def ... return ...:
<name> = lambda <args>: <expr>
Comme son nom l'indique, une expression lambda est une expression Python standard, mis à part que sa valeur va dépendre de paramètres qui ne seront connus qu'au moment de l'évaluation. On peut donc voir une expression lambda comme une fonction Python extrêmement simplifiée, dont le code se limite à l'évaluation d'une seule expression, sans pouvoir utiliser d'instructions (ni affectation, ni test, ni boucle...). Une expression lambda peut renvoyer plusieurs valeurs sous la forme d'un tuple, mais dans ce cas, il est impératif de mettre la séquence des valeurs à renvoyer entre parenthèses.
Une expression lambda est quasiment équivalente à la notion de fonction mathématique), alors que les fonctions et les générateurs, utilisés par les langages de programmation correspondent à une généralisation de cette notion, comme indiqué plus haut, à cause des structures de contrôle qu'elle peuvent inclure. Il faut noter que dans le domaine des Sciences des Données, les expressions lambda sont employées de manière extrêmement fréquente car elles sont parfaitement adaptées pour formaliser chacun des maillons élémentaires mise en oeuvre dans une chaîne de traitement de données.
sqrt = lambda x: x ** 0.5 # racine carrée
cbrt = lambda x: x ** (1/3) # racine cubique
minmax = lambda *args: (min(*args), max(*args)) # min et max combinés (parenthèses obligatoires)
join = lambda seq, sep=' ': sep.join(str(val) for val in seq) # conversion d'une séquence en chaîne
show("sqrt(16); cbrt(27); minmax(4,27,3,16); join(range(10,55,5), ':')")
sqrt(16) ➤ 4.0 cbrt(27) ➤ 3.0 minmax(4,27,3,16) ➤ (3, 27) join(range(10,55,5), ':') ➤ 10:15:20:25:30:35:40:45:50
4 - Documentation et annotation¶
Lorsqu'on écrit le code d'une fonction ou d'un générateur, il est évidemment très important de fournir des indications sur la manière d'utiliser ce code. Trois éléments d'information sont généralement utiles :
- Le rôle exact de la fonction
- L'ordre, la nature et le domaine de validité pour les différents arguments à fournir en entrée
- L'ordre, la nature et le domaine de validité pour les valeurs en sortie de la fonction
Pour les langages à typage dynamiques, ce type d'information est généralement fournie dans la docstring de la fonction, avec une syntaxe variable selon les langages et selon les développeurs. Une docstring est simplement une chaîne de caractères, définie sur la ligne qui suit celle de l'entête de la fonction, et destinée à fournir les trois éléments d'information listés ci-dessus.
Dans le cas du langage Python, il existe plusieurs conventions sur la manière de formater les docstrings. Pour les fonctions simples, on se contente généralement d'une docstring compacte, c'est-à-dire restreinte à une seule ligne (dans un notebook Jupyter, cela correspond environ à 100 caractères; dans un éditeur de code standard, cela correspond à 80, 100 ou 120 caractères selon la configuration de l'éditeur). Cette docstring est chargée de résumer le rôle de la fonction et (éventuellement, s'il reste de la place) de fournir des indications sur le rôle et la nature des arguments. Pour les fonctions complexes, on utilise une docstring complète (définie sur plusieurs lignes, en incluant une description exhaustive des arguments, des valeurs renvoyées, des erreurs et exceptions éventuelles, etc) et dans ce cas, notamment dans le domaine des Sciences des Données, les docstrings vont généralement respecter le formalisme défini par la bibliothèque numpy (appelé numpydoc style) qui sera étudiée en détail dans le chapitre 5.
Dans le cas de la fonction facto définie plus haut, voici un exemple de docstring compacte :
# ajout d'une docstring compacte (= restreinte à une seule ligne) au code de 'facto'
# on donne le rôle de la fonction ainsi que le domaine de validité de l'argument 'n'
# la convention préconise d'utiliser systématiquement des triples guillemets
def facto(n):
"""return n! (factorial of integer n >= 0)"""
out = 1
for loop in range(1, n+1): out *= loop
return out
La version 3.6 de Python a introduit le principe des annotations qui offre une syntaxe standardisée pour fournir à l'utilisateur des informations sur les données en entrée et en sortie d'une fonction :
Chaque argument de fonction
<arg>reçoit un suffixe de la forme:<type>(lorsque l'argument ne prend pas de valeur par défaut) ou:<type>=<val>(lorsqu'il prend la valeur<val>par défaut)Entre la parenthèse fermante des arguments de fonction, et le caractère
:qui introduit le bloc de la fonction, on rajoute un suffixe de la forme-> <type>pour indiquer le type de retour.
Voici le code modifié de la fonction facto en y incluant les annotations :
def facto(n:int) -> int: # annotation de fonction (par type prédéfini)
"""return n! (factorial of n >= 0)""" # plus besoin d'indiquer le type de l'argument 'n'
out : int = 1 # annotation de variable (par type prédéfini)
for loop in range(1, n+1): out *= loop
return out
N'importe quel type de données connu de l'interpréteur au moment de l'exécution de la fonction peut être utilisé pour les annotations : les types standards du langage, les types importés via des modules, ou même des types définis dans le code via des classes. Néanmoins, il arrive assez fréquemment que plusieurs types puissent être acceptés pour un argument donné, ou que dans le cas d'un conteneur, seuls certains types soient autorisés pour les éléments stockés dans ce conteneur. Pour ces cas de figure, le module typing de la bibliothèque standard fournit des outils spécifiques permettre une annotation précise, mais au prix d'une syntaxe assez lourde.
Pour les fonctions définies dans les cellules de code d'un notebook, on va généralement mettre en oeuvre une alternative plus flexible consistant à utiliser des chaînes de caractères pour lister les types de données possibles. Cela donne une syntaxe plus légère, mais surtout cela offre la possibilité d'ajouter d'autres contraintes (notamment sur le domaine des valeurs utilisées) directement dans les annotations. De plus, en mettant ces éléments dans les annotations, cela permet de dégager de la place sur la ligne unique d'une docstring compacte pour y mettre un résumé plus complet du rôle de la fonction.
Voici ce que cela donne pour la fonction facto :
def facto(n:'int >= 0') -> 'int > 0': # annotation de fonction (par chaîne de caractères)
"""return n! (factorial of n)""" # plus besoin d'indiquer ni le type ni le domaine pour 'n'
out : 'int > 0' = 1 # annotation de variable (par chaîne de caractères)
for loop in range(1, n+1): out *= loop
return out
Avec cette syntaxe, on peut avoir une information compacte et précise sur facto via la fonction standard help :
help(facto)
Help on function facto in module __main__:
facto(n: 'int >= 0') -> 'int > 0'
return n! (factorial of n)
Il est important de noter que les annotations sont uniquement destinés à un usage de documentation du code, l'interpréteur Python ne fait aucune vérification pour garantir que les arguments et les retours sont bien conformes aux types de données ou aux domaines de validité qui ont été indiqués. Si on souhaite effectuer cette vérification, il faudra rajouter des instructions spécifiques dans le code de la fonction, notamment via l'instruction assert comme nous le verrons dans le chapitre 4.
Les dernières versions du langage Python ont commencé à incorporer des outils (comme le mot-clé type ajouté dans la version 3.12 de Python, et le package mypy permettant la vérification des types) destinés à obtenir un meilleur contrôle sur les types de données, assez similaires à ce que fournissent des langages à typage statique. Néanmoins ces nouvelles fonctionnalités sont encore très mouvantes (la syntaxe évolue au fur et à mesure des différentes versions) et, pour le moment, sont assez peu adaptées à la création de code dans des notebooks pour les applications en Sciences des Données.
E - Fichiers textes et fichiers binaires
Le langage Python permet la manipulation des deux grandes familles de fichiers, classiquement utilisés en informatique :
- les fichiers textes dont le contenu est une séquence de caractères, équivalent au type
str - les fichiers binaires dont le contenu est une séquence d'octets, équivalent au type
bytes
Chacune de ses deux familles de fichiers se décompose en un grand nombre de formats différents, selon la nature des données qui y sont stockés. Un usage quasiment universel consiste à rajouter un suffixe .xxx à la fin du nom des fichiers, où xxx est un acronyme standardisé qui va permettre d'identifier facilement la nature des données d'un fichier, aussi bien pour le système d'exploitation que pour les utilisateurs humains. Il existe aujourd'hui plusieurs milliers de formats de fichiers différents, mais dans le domaine des Sciences des Données, seule une petite vingtaine de formats est utilisée de manière intensive :
Formats de fichiers standards en mode texte¶
.txt(Raw Text) : format sans contenu standardisé, souvent manipulé ligne par ligne.csv(Comma Separated Values) ou.tsv(Tab Separated Values) ou.ssv(Space Separated Values) : formats utilisés pour stocker les données structurées de manière matricielle (issues d'un tableur ou d'une base de données).json(JavaScript Object Notation) ou.xml(Extensible Markup Language) ou.yaml(YAML Ain't Markup Language) : formats utilisés pour stocker les données structurées de manière hiérarchique (arbres, graphes, forêts).ini(Windows Initialization File) ou.conf(Unix Config File) ou.toml(Tom's Obvious Minimal Language) : formats utilisés comme fichiers de configuration pour une application.sql(Structured Query Language) : format utilisé pour stocker les tables d'une base de données relationnelle (compatible avec tout système de gestion basé sur le langage SQL).py(Python) ou.java(Java) ou.js(JavaScript) ou.r(R) ou autres... : formats identifiant le code source des différents langages de programmation.html(HyperText Markup Language) ou.css(Cascading Style Sheets) ou.md(Markdown) ou.rst(reStructuredText) ou.tex(LaTeX) ou autres... : formats identifiant le code source des différents langages de présentation
Formats de fichiers standards en mode binaire¶
.zipou.rarou.7zou.tarou.tgzou.iso: formats d'archivage permettant de regrouper toute une arborescence de fichiers en un seul fichier, souvent en ajoutant de la compression sans pertes pour diminuer la taille globale.jpg(Joint Photographic Experts Group) ou.png(Portable Network Graphics) ou.gif(Graphics Interchange Format) ou.tiff(Tagged Image File Format) ou.webp(Web Picture) : formats utilisés pour le stockage d'images.wav(Waveforme Audio File) ou.mp3(MPEG Audio Layer 3) ou.ogg(Ogg Vorbis) ou.aac(Advanced Audio Coding) : formats utilisés pour le stockage de sons.mp4(MPEG Video Layer 4) ou.avi(Audio Video Interleave) ou.mkv(Matroska Video) ou.webm(Web Media) : formats utilisés pour le stockage de vidéos.docxou.xlsxou.pptx: formats utilisés par la suite Microsoft Office depuis 2007. Comme il s'agit de formats ouverts (les 3 formats sont tout simplement des fichiers XML compressés au format ZIP, à la différence des anciens formats.doc,.xlset.pptqui étaient propriétaires) leur usage s'est généralisé pour l'échanges de documents car de nombreuses applications peuvent manipuler ces formats..hdf5(Hierarchical Data Format 5) : format utilisé pour le stockage de données structurée de manière hiérarchique. Globalement, c'est une alternative aux formats JSON ou XML à utiliser lorsque les données contiennent de nombreux fichiers binaires volumineux (images, sons, vidéos).sqlite(SQLite File) : format utilisé pour le stockage des bases de données locales (initialement créé pour l'environnement SQLite, il est aujourd'hui un standard de fait utilisé par de très nombreux applications de traitement de données
Comme nous allons le voir dans les chapitres suivants, la plupart de ces formats de fichiers sont facilement manipulables (en lecture et en écriture) par du code écrit en langage Python, notamment via les packages display (cf. chapitre 4) et pandas (cf. chapitre 7) qui sont directement disponibles dans l'environnement JupyterLab. Ces packages fournissent des outils de haut-niveau, soit pour récupérer les données dans les fichiers et les convertir automatiquement vers des structures de données Python, soit pour faire l'opération inverse.
Par contre, si on se restreint au noyau Python, seules des opérations beaucoup plus basiques sont disponibles : dans un fichier ouvert en mode texte, on peut uniquement lire ou écrire un flux de données de type str , et dans un fichier ouvert en mode binaire, on peut uniquement lire ou écrire un flux de données de type bytes . Parmi tous les formats standards listés ci-dessus, seul le format .txt est donc véritablement utilisable avec ces outils de base. Pour tous les autres formats, on va systématiquement avoir recours aux packages externes afin de simplifier les manipulations.
1 - Lecture et écriture d'un fichier texte (données non-structurées)¶
La lecture/écriture d'un fichier s'effectue toujours en 3 étapes :
- on ouvre le fichier en précisant le mode d'accès : soit en mode lecture, soit en mode création, soit en mode rajout. Cette opération s'effectue avec la fonction standard
open(filename, mode)oùfilenamereprésente le chemin d'accès du fichier sur disque, etmodereprésente le mode d'accès qui possède trois possibilités au choix :
'r'= lecture (read) ●'w'= création (write) ●'a'= ajout (append) - on lit/crée/rajoute des données dans le fichier, avec les méthodes
read(...)ouwrite(...) - on ferme le fichier avec la méthode
close()pour permettre au système d'exploitation de mettre à jour le fichier sur disque. En pratique, cette étape peut être rendue automatique en utilisant un blocwithpour la gestion du fichier (cf. exemples ci-dessous)
Dans les premières versions de Python, seules des chaînes de caractères au format ASCII pouvaient être lues et écrites dans un fichier texte, donc la question de l'encodage des caractères ne se posait pas. Depuis, la gestion des fichiers texte s'est généralisée, et la fonction open s'est vue rajouter un paramètre encoding qui va préciser l'encodage utilisé pour les chaînes de caractères stockées dans le fichier. Depuis une quinzaine d'années, la norme UTF-8 du standard Unicode s'impose de plus en plus comme le standard universel pour l'encodage des fichiers textes (comme on l'a vu plus haut, c'est également l'encodage utilisé par Python pour stocker ses chaînes de caractères en mémoire). Par conséquent, il est très fortement conseillé de rajouter systématiquement le paramètre encoding='utf8' ou encoding='utf-8' (les deux variantes sont équivalentes) à chaque appel de la fonction open pour assurer que le contenu sera bien encodé avec ce standard.
Néanmoins, même de nos jours, il existe encore de nombreuses applications qui produisent des fichiers textes qui ne sont pas encodés en UTF-8, et par conséquent, de nombreux fichiers de données que vous allez croiser vont devoir être décodés avec une autre valeur pour le paramètre encoding. La liste complète des encodages reconnus par Python (près d'une centaine !) se trouve sur cette page du manuel de référence de la bibliothèque standard du langage, mais en pratique (si on se restreint aux fichiers écrits dans les langues occidentales), les quatre encodages non-Unicode les plus fréquents sont latin1 , latin9 , ansi et macroman. Ces quatre encodages utilisent une table fixe de 256 caractères où les 128 premiers caractères correspondent au standard ASCII, et les 128 caractères suivants (qui contiennent les lettres accentuées, et d'autres symboles spécifiques) sont différents entre les quatre familles.
from SRC.tools import cutcut # import de la fonction 'cutcut' permettant de découper les chaines longues
# création d'une chaîne de caractères assez longue pour remplir notre fichier texte
text = '\n'.join(f"{n}! = {facto(n)}" for n in range(100)) # toutes les factorielles entre 0! et 99!
print(f"Nombre de caractères = {len(text)}") # calcul de la taille de la chaîne
print(cutcut(text, head=6, tail=2)) # affichage des 6 premières et 2 dernières lignes de la chaîne créée
Nombre de caractères = 7523 0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 ● ● ● 98! = 9426890448883247745626185743057242473809693764078951663494238777294707070023223798882976159207729119823605850588608460429412647567360000000000000000000000 99! = 933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272237582511852109168640000000000000000000000
with open('TEST/factos.txt', 'w', encoding='utf8') as file: # ouverture d'un fichier texte en mode 'write'
file.write(text) # écriture de l'intégralité de la chaîne précédente dans le fichier
with open('TEST/factos.txt', 'r', encoding='utf8') as file: # ouverture d'un fichier texte en mode 'read'
text = file.read() # lecture de l'intégralité du fichier en tant que chaîne multi-lignes
print(cutcut(text, 6, 2)) # affichage des 6 premières et 2 dernières lignes de la chaîne obtenue
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 ● ● ● 98! = 9426890448883247745626185743057242473809693764078951663494238777294707070023223798882976159207729119823605850588608460429412647567360000000000000000000000 99! = 933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272237582511852109168640000000000000000000000
lines = text.split('\n') # conversion en liste de lignes (split sur '\n')
print(f"{lines[0]}\n...\n{lines[-1]}") # affichage première et dernière ligne de la liste
0! = 1 ... 99! = 933262154439441526816992388562667004907159682643816214685929638952175999932299156089414639761565182862536979208272237582511852109168640000000000000000000000
with open('TEST/factos.txt', 'a', encoding='utf8') as file: # ouverture fichier texte en mode 'append'
for n in range(100, 200): file.write(f"\n{n}! = {facto(n)}") # on complète ligne par ligne
# Note : la méthode 'write' n'effectue pas de retour à la ligne, il faut rajouter '\n' explicitement
# relecture complète du fichier et conversion en liste de lignes pour vérifier les modifications
with open('TEST/factos.txt', 'r', encoding='utf8') as file: lines = file.read().split('\n')
print(f"{lines[0]}\n...\n{lines[-1]}") # affichage première et dernière ligne
0! = 1 ... 199! = 3943289336823952517761816069660925311475679888435866316473712666221797249817016714601521420059923119520886060694598194151288213951213185525309633124764149655567314286353816586186984944719612228107258321201270166459320656137141474266387621212037869516201606287027897843301130159520851620311758504293980894611113948118519486873600000000000000000000000000000000000000000000000
2 - Lecture et écriture d'un fichier texte (données semi-structurées)¶
Dans le domaine des Sciences des Données, les fichiers textes sont très souvent utilisés pour stocker des données semi-structurées, c'est-à-dire avec une organisation plus flexible que les tables, les arbres ou les graphes. Ce type de fichier est habituellement conçu pour être à la fois facile à lire pour un être humain, et facile à traiter pour un algorithme (un exemple typique est le questionnaire d'auto-évaluation que vous avez rempli à la rentrée).
Comme il n'y a pas de format standardisé pour ces fichiers (concrètement, chaque application définit un formatage ad-hoc en fonction des données utilisées), il n'existe évidemment pas de module Python permettant leur gestion. Néanmoins, les différentes variantes de fichiers semi-structurés se basent sur un certains nombre de points communs :
- Un fichier semi-structuré est systématiquement décomposé en lignes (donc le caractère
\nsera utilisé comme séparateur de lignes) - Parfois ces lignes sont elles-mêmes regroupées en blocs dont la fin est identifiée par une ligne vide (donc le double caractère
\n\nsera utilisé comme séparateur de blocs) - En plus des données, le fichier va généralement inclure des commentaires permettant de fournir des informations supplémentaires au lecteur humain. De manière quasi-générale, le formalisme pour ces commentaires reprend celui utilisé par de nombreux langages : un caractère spécifique (très souvent
#comme en Python) placé au début d'une ligne permet de transformer cette ligne en une ligne de commentaire, qui sera donc ignoré par le processus de traitement - Lorsqu'il y a une structure à deux niveaux (blocs et lignes), il est assez classique de définir également un caractère spécifique pour les blocs de commentaires, ce qui permet de transformer très simplement un bloc de données en un bloc de commentaires, et vice versa
Dans la suite de ce cours, on va ainsi utiliser très fréquemment les deux fonctions load et save du module tools , qui permettent de simplifier énormément l'écriture et lecture de fichiers textes contenant des données semi-structurées. Comme on l'a vu pour la fonction show au début du chapitre, il faut commencer par signaler l'existence de ces fonctions au noyau d'exécution du notebook, à l'aide de la commande import :
from SRC.tools import load, save # import des fonctions 'load' et 'save' du module 'tools'
help(load); help(save) # un peu d'aide sur l'utilisation de ces fonctions
Help on function load in module SRC.tools:
load(location, split='\n', comment='#', subsplit='', subcomment='', strip=True, encoding='utf8')
load content of provided text file and perform split/strip/clean tasks
- location:str = path to local file or url to distant file via HTTP or HTTPS
- split:str = pattern used as first level separator (None or '' means no split)
- comment:str = pattern used as comment prefix on split level
- subsplit:str = pattern used as second level separator (None or '' means no subsplit)
- subcomment:str = pattern used as comment prefix on subsplit level
- strip:bool = remove boundary whitespaces for whole content and for each split level
- encoding:str = character encoding used for file reading
Help on function save in module SRC.tools:
save(location, body='', head='', append=False, comment='# ', encoding='utf8')
save string or iterable into text file with optional comment header
- location:str = absolute or relative path to local file
- body:str|list|tuple = content for file body (converted into data lines)
- head:str|list|tuple = content for file head (converted into comment lines)
- append:bool : append head+body at existing content, instead of overwrite
- comment:char = prefix char defining comment lines
- encoding:str = character encoding used for file
Comme un fichier texte semi-structuré contient presque systématiquement des données organisées en lignes, le comportement par défaut de la fonction load est de lire l'intégralité du contenu du fichier et de le renvoyer sous forme d'une liste de lignes, sans avoir besoin ni d'ouvrir et fermer explicitement le fichier, ni de spécifier l'encodage (car 'utf8' est utilisé par défaut). Si on reprend l'exemple du fichier factos.txt créé précédemment, voici le résultat obtenu avec les paramètres par défaut de load() :
lines = load('TEST/factos.txt') # lecture du fichier avec conversion automatique en liste de lignes
print(f"{lines[0]}\n...\n{lines[-1]}") # affichage première et dernière ligne du fichier
0! = 1 ... 199! = 3943289336823952517761816069660925311475679888435866316473712666221797249817016714601521420059923119520886060694598194151288213951213185525309633124764149655567314286353816586186984944719612228107258321201270166459320656137141474266387621212037869516201606287027897843301130159520851620311758504293980894611113948118519486873600000000000000000000000000000000000000000000000
Si on souhaite récupérer le contenu du fichier sous la forme d'une unique chaîne de caractères multi-lignes, il suffit de mettre une chaîne vide comme séparateur de lignes, grâce au paramètre optionnel split='' :
text = load('TEST/factos.txt', split='') # split='' annule la conversion en liste de lignes
print(text[:100]) # affichage des 100 premiers caractères de la chaîne multi-lignes obtenue
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800
De la même manière, le comportement par défaut de la fonction save est de prendre soit une chaîne multi-lignes, soit une liste de lignes, et de la recopier dans un fichier texte. Mais la fonction admet également un argument head qui permet de rajouter une autre liste de lignes en entête du fichier (d'où le nom du paramètre) sous forme de lignes de commentaires :
head = f"""{'-'*80}
Calcul de la suite des factorielles pour les nombres entiers
Auteur : Christophe Schlick
Date : 2024/09/01
{'-'*80}""" # création d'une chaîne multi-lignes destinée à servir d'entête au fichier
save('TEST/factos.txt', lines, head=head) # écriture du fichier en rajoutant l'entête
text = load('TEST/factos.txt', split='') # lecture du fichier sans conversion en liste de lignes
print(text[:314]) # affichage des 314 premiers caractères, incluant les lignes de commentaires
# -------------------------------------------------------------------------------- # Calcul de la suite des factorielles pour les nombres entiers # Auteur : Christophe Schlick # Date : 2024/09/01 # -------------------------------------------------------------------------------- 0! = 1 1! = 1 2! = 2 3! = 6 4! = 24
La fonction load() définit par défaut l'argument comment='#' ce qui signifie que toute ligne commençant par un # sera identifiée comme ligne de commentaire, et ne sera donc pas insérée dans la liste des lignes retournées par la fonction :
lines = load('TEST/factos.txt') # lecture du fichier avec conversion en liste de lignes
for line in lines[:10]: print(line) # avec élimination automatique des lignes de commentaires
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880
Si l'on souhaite convertir le contenu du fichier en liste de lignes, tout en conservant les lignes de commentaires, il suffit de mettre une chaîne vide dans le paramètre comment='' lors de l'appel de la fonction load() :
lines = load('TEST/factos.txt', comment='') # comment = '' annule l'élimination des lignes de commentaires
for line in lines[:10]: print(line) # affichage des 10 premières lignes, incluant les commentaires
# -------------------------------------------------------------------------------- # Calcul de la suite des factorielles pour les nombres entiers # Auteur : Christophe Schlick # Date : 2024/09/01 # -------------------------------------------------------------------------------- 0! = 1 1! = 1 2! = 2 3! = 6 4! = 24
3 - Lecture et écriture d'un fichier binaire (données non-structurées)¶
Pour le noyau Python, les fichiers textes et les fichiers binaires se manipulent de manière quasi-équivalente, avec simplement deux légères différences de syntaxe :
- On utilise le type
bytespour stocker les données des fichiers binaires, au lieu du typestrpour les fichiers textes - Les trois modes d'ouverture en lecture, écriture, édition sont respectivement identifiés par les acronymes
'rb','wb'et'ab'pour les fichiers binaires (le suffixe 'b' signifiant binaire, évidemment)
# création d'une séquence d'octets assez longue pour remplir notre fichier binaire
from random import randrange as rr # import de la fonction 'randrange' avec alias 'rr'
seq = bytes(rr(256) for loop in range(1000)) # création d'une séquence aléatoire de 1000 octets
print(f"Nombre d'octets = {len(seq)}") # calcul de la taille de la séquence --> 1000 octets
print(f"{seq[:10]}\n...\n{seq[-10:]}") # affichage début et fin de séquence
print(f"{list(seq[:10])}\n...\n{list(seq[-10:])}") # idem mais avec conversion en liste d'entiers
Nombre d'octets = 1000 b'tt\x06\x06\x0c\xcaG\xc0\xea\x03' ... b'\xcex\xdez\x03\x89\x8e\xd0\x86\xa9' [116, 116, 6, 6, 12, 202, 71, 192, 234, 3] ... [206, 120, 222, 122, 3, 137, 142, 208, 134, 169]
with open('TEST/random', 'wb') as file: # ouverture d'un fichier binaire en mode 'write'
file.write(seq) # écriture de la séquence d'octets dans le fichier
# Note : par convention, les noms des fichiers binaires non-structurés n'ont pas d'extension
with open('TEST/random', 'rb') as file: # ouverture d'un fichier binaire en mode 'read'
seq = file.read() # lecture de l'intégralité du fichier en tant que séquence d'octets
print(f"Nombre d'octets = {len(seq)}") # calcul de la taille de la séquence --> 1000 octets
print(f"{seq[:10]}\n...\n{seq[-10:]}") # affichage début et fin de séquence
Nombre d'octets = 1000 b'tt\x06\x06\x0c\xcaG\xc0\xea\x03' ... b'\xcex\xdez\x03\x89\x8e\xd0\x86\xa9'
with open('TEST/random', 'ab') as file: # ouverture d'un fichier binaire en mode 'append'
for n in range(4): file.write(seq) # on rajoute 4 fois la séquence initiale
with open('TEST/random', 'rb') as file: seq = file.read() # lecture du fichier modifié
print(f"Nombre d'octets = {len(seq)}") # calcul de la taille de la séquence --> 5000 octets
print(f"{seq[:10]}\n...\n{seq[-10:]}") # affichage début et fin de séquence
Nombre d'octets = 5000 b'tt\x06\x06\x0c\xcaG\xc0\xea\x03' ... b'\xcex\xdez\x03\x89\x8e\xd0\x86\xa9'
4 - Lecture et écriture d'un fichier binaire (données semi-structurées)¶
Comme pour les fichiers textes, la lecture et l'écriture de fichiers binaires contenant des données non-structurées est rarement utilisée en pratique, et est habituellement remplacé par un format de fichier qui rajoute des informations complémentaires sur l'organisation des données stockées dans le fichier. Le langage Python possède un module appelé pickle qui a été spécifiquement développé pour le stockage de données semi-structurées, et qui fournit deux fonctions principales :
- La fonction
dumppermet de sauvegarder dans un fichier binaire sur disque, à la fois le type et la valeur d'une variable Python de nature arbitraire (on utilise habituellement l'extension.pklpour les fichiers au format pickle) - La fonction
loadpermet de recharger dans une variable Python en mémoire, les informations précédemment stockées dans un fichier binaire au format pickle
Ce processus s'appelle la sérialisation et, dans le domaine des Sciences des Données, il est notamment utilisé pour sauvegarder des résultats intermédiaires d'un calcul en cours, et permettre ainsi de fragmenter un traitement complexe sur plusieurs sessions de calculs, sans être obligé de repartir du début à chaque fois. L'avantage du module pickle est qu'il propose un format très compact, et un processus très efficace, pour la sérialisation des données manipulés par un programme, quelque soit le domaine d'application considéré. A l'inverse, son inconvénient principal vient du fait qu'il s'agit d'un format spécifique au langage Python, donc les données sérialisées avec pickle ne pourront pas être facilement récupérées via un autre langage de programmation. C'est pour cela que la tendance actuelle est plutôt de privilégier des formats de sérialisation basés sur des fichiers textes (tels que JSON ou YAML) qui sont compatibles avec tout langage de programmation, mais qui ont l'inconvénient d'être beaucoup plus volumineux que leur équivalent généré avec pickle.
Le nom du module
pickleest une référence volontaire aux bocaux de cornichons (pickle en anglais). En effet, l'objectif du module est de conserver des données sur une longue durée, même après avoir arrêté le programme ou éteint l'ordinateur. De la même manière, la saumure utilisée dans un bocal de cornichons va conserver son contenu sans qu'il ne s'altère ou ne dégénère, même sur une longue période.
dic = dict(zip(range(200), factos(200))) # dictionnaire stockant les 200 premières factorielles
show("len(dic); dic[0]; dic[1]; dic[2];; dic[199]") # affichage de quelques couples (clé,valeur)
len(dic) ➤ 200 dic[0] ➤ 1 dic[1] ➤ 1 dic[2] ➤ 2 dic[199] ➤ 3943289336823952517761816069660925311475679888435866316473712666221797249817016714601521420059923119520886060694598194151288213951213185525309633124764149655567314286353816586186984944719612228107258321201270166459320656137141474266387621212037869516201606287027897843301130159520851620311758504293980894611113948118519486873600000000000000000000000000000000000000000000000
from pickle import load, dump # import des fonctions 'load' et 'dump' du module 'pickle'
help(load); help(dump) # un peu d'aide sur l'utilisation de ces fonctions
Help on built-in function load in module _pickle:
load(file, *, fix_imports=True, encoding='ASCII', errors='strict', buffers=())
Read and return an object from the pickle data stored in a file.
This is equivalent to ``Unpickler(file).load()``, but may be more
efficient.
The protocol version of the pickle is detected automatically, so no
protocol argument is needed. Bytes past the pickled object's
representation are ignored.
The argument *file* must have two methods, a read() method that takes
an integer argument, and a readline() method that requires no
arguments. Both methods should return bytes. Thus *file* can be a
binary file object opened for reading, an io.BytesIO object, or any
other custom object that meets this interface.
Optional keyword arguments are *fix_imports*, *encoding* and *errors*,
which are used to control compatibility support for pickle stream
generated by Python 2. If *fix_imports* is True, pickle will try to
map the old Python 2 names to the new names used in Python 3. The
*encoding* and *errors* tell pickle how to decode 8-bit string
instances pickled by Python 2; these default to 'ASCII' and 'strict',
respectively. The *encoding* can be 'bytes' to read these 8-bit
string instances as bytes objects.
Help on built-in function dump in module _pickle:
dump(obj, file, protocol=None, *, fix_imports=True, buffer_callback=None)
Write a pickled representation of obj to the open file object file.
This is equivalent to ``Pickler(file, protocol).dump(obj)``, but may
be more efficient.
The optional *protocol* argument tells the pickler to use the given
protocol; supported protocols are 0, 1, 2, 3, 4 and 5. The default
protocol is 4. It was introduced in Python 3.4, and is incompatible
with previous versions.
Specifying a negative protocol version selects the highest protocol
version supported. The higher the protocol used, the more recent the
version of Python needed to read the pickle produced.
The *file* argument must have a write() method that accepts a single
bytes argument. It can thus be a file object opened for binary
writing, an io.BytesIO instance, or any other custom object that meets
this interface.
If *fix_imports* is True and protocol is less than 3, pickle will try
to map the new Python 3 names to the old module names used in Python
2, so that the pickle data stream is readable with Python 2.
If *buffer_callback* is None (the default), buffer views are serialized
into *file* as part of the pickle stream. It is an error if
*buffer_callback* is not None and *protocol* is None or smaller than 5.
with open('TEST/factos.pkl','wb') as file: # ouverture d'un fichier binaire en mode 'write'
dump(dic, file) # écriture du dictionnaire 'dic' au format pickle dans le fichier 'file'
with open('TEST/factos.pkl','rb') as file: # ouverture d'un fichier binaire en mode 'read'
doc = load(file) # lecture du contenu du fichier pickle et stockage dans la variable 'doc'
show("type(doc)") # le type de la variable stockant le contenu du fichier est bien un dictionnaire
show("len(doc); doc[0]; doc[1]; doc[2];; doc[199]") # affichage de quelques éléments du dictionnaire
type(doc) ➤ <class 'dict'> len(doc) ➤ 200 doc[0] ➤ 1 doc[1] ➤ 1 doc[2] ➤ 2 doc[199] ➤ 3943289336823952517761816069660925311475679888435866316473712666221797249817016714601521420059923119520886060694598194151288213951213185525309633124764149655567314286353816586186984944719612228107258321201270166459320656137141474266387621212037869516201606287027897843301130159520851620311758504293980894611113948118519486873600000000000000000000000000000000000000000000000
Remarque importante : Dans le paragraphe E2, nous avons importé les fonctions load et save du module tools, et dans le paragraphe E4, les fonctions load et dump du module pickle. Comme la fonction load porte le même nom dans les deux modules, le fait d'importer la seconde fonction load a automatiquement écrasé la première, qui est donc devenu inaccessible. Ce phénomène clairement non souhaitable s'appelle collision de nom (name collision). La probabilité d'avoir deux fonctions portant le même nom dans deux modules différents, sera évidemment fortement augmentée si le code Python utilise de nombreux modules différents (ce sera le cas pour quasiment toutes les applications dans le domaine des Sciences de Données). La solution, comme nous le verrons dès le chapitre suivant, consiste à utiliser un espace de noms différent pour stocker les identificateurs pour chacun des modules importés.
F - Bibliothèque standard et modules externes
En dehors des fonctionnalités fournies par le noyau du langage, l'autre raison de la popularité de Python est liée à la richesse de la bibliothèque de fonctions disponibles en standard lors de l'installation de l'interpréteur. Dans les versions récentes de Python, cette bibliothèque comporte plus de 8000 fonctions réparties en près de 250 modules différents.
Dans la terminologie de Python, un module est un fichier de code qui regroupe un certain nombre de fonctionnalités que le développeur doit importer avant de pouvoir les utiliser. Le contenu de ces modules est différent selon le paradigme de programmation utilisé :
- avec un paradigme procédural, un module regroupe des fonctions et (plus rarement) des constantes
- avec un paradigme objet, un module regroupe des classes et (plus rarement) quelques instances prédéfinies.
En plus de cette bibliothèque standard, disponible par défaut à l'installation de l'environnement Python, il existe à ce jour plus de 450000 modules externes regroupant largement plusieurs millions de fonctions qui ont été développés pour le langage Python. Selon le nombre de fonctionnalités qu'ils proposent, ces modules sont généralement regroupés de manière thématique dans des packages (le terme français paquetage existe, mais est d'un usage assez rare) qui eux-mêmes peuvent être regroupés dans des packages de packages, et ainsi de suite, de manière hiérarchique. L'environnement Python fournit un outil de gestion de packages, appelé pip (acronyme récursif : pip installs packages) qui permet le téléchargement de ces packages sur le web, suivi de l'installation dans l'environnement de l'utilisateur.
Note : La distribution Anaconda est installée avec son propre gestionnaire de packages, appellé conda, qui a pour avantage d'être compatible avec d'autres langages de programmation que Python (notamment R, Julia, Ruby, Java, C/C++), ce qui lui permet d'installer des bibliothèques hybrides, combinant du code écrit dans plusieurs langages. Pendant une assez longue période, l'utilisation conjointe de pip et de conda sur un même environnement Python, engendrait à coup sûr des erreurs et des instabilités. Ces incompatiblités ont été corrigés depuis les versions récentes des deux gestionnaires de packages
Etant donné le nombre gigantesque de modules Python existants, il est évidemment totalement illusoire d'espérer connaître l'intégralité de ces modules par coeur, même si on se limite à la seule bibliothèque standard. Par conséquent, il est important d'apprendre à naviguer dans la documentation disponible en anglais et en français sur le site de Python. Dans ce chapitre, nous allons simplement effectuer un tour d'horizon rapide de la bibliothèque standard de Python, en se focalisant sur les éléments les plus utiles dans le domaine de la Sciences des Données. Les autres modules externes seront étudiés dans les chapitres suivants, là encore, en se limitant à ceux qui sont les plus utiles dans le domaine des Sciences des Données.
1 - Accès à la documentation des modules¶
Le bon réflexe lorsqu'on importe un module est d'aller consulter la documentation correspondante sur le site web associé à ce module. Lorsqu'on travaille avec l'environnement JupyterLab, la documentation des modules les plus fréquemment utilisés dans le domaine des Sciences des Données (bibliothèque Python standard, ipython, numpy, scipy, sympy, pandas, matplotlib) est directement accessible via l'onglet Help de la barre de menus.
Une autre manière d'obtenir de l'information sur un module est de visualiser sa docstring complète via la fonction help(nom_du_module). Attention, cela ne fonctionne que si le module a été importé au préalable :
import string # import du module 'string' de la bibliothèque standard
help(string) # affichage de la docstring complète du module 'string'
Help on module string:
NAME
string - A collection of string constants.
MODULE REFERENCE
https://docs.python.org/3.12/library/string.html
The following documentation is automatically generated from the Python
source files. It may be incomplete, incorrect or include features that
are considered implementation detail and may vary between Python
implementations. When in doubt, consult the module reference at the
location listed above.
DESCRIPTION
Public module variables:
whitespace -- a string containing all ASCII whitespace
ascii_lowercase -- a string containing all ASCII lowercase letters
ascii_uppercase -- a string containing all ASCII uppercase letters
ascii_letters -- a string containing all ASCII letters
digits -- a string containing all ASCII decimal digits
hexdigits -- a string containing all ASCII hexadecimal digits
octdigits -- a string containing all ASCII octal digits
punctuation -- a string containing all ASCII punctuation characters
printable -- a string containing all ASCII characters considered printable
CLASSES
builtins.object
Formatter
Template
class Formatter(builtins.object)
| Methods defined here:
|
| check_unused_args(self, used_args, args, kwargs)
|
| convert_field(self, value, conversion)
|
| format(self, format_string, /, *args, **kwargs)
|
| format_field(self, value, format_spec)
|
| get_field(self, field_name, args, kwargs)
| # given a field_name, find the object it references.
| # field_name: the field being looked up, e.g. "0.name"
| # or "lookup[3]"
| # used_args: a set of which args have been used
| # args, kwargs: as passed in to vformat
|
| get_value(self, key, args, kwargs)
|
| parse(self, format_string)
| # returns an iterable that contains tuples of the form:
| # (literal_text, field_name, format_spec, conversion)
| # literal_text can be zero length
| # field_name can be None, in which case there's no
| # object to format and output
| # if field_name is not None, it is looked up, formatted
| # with format_spec and conversion and then used
|
| vformat(self, format_string, args, kwargs)
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
class Template(builtins.object)
| Template(template)
|
| A string class for supporting $-substitutions.
|
| Methods defined here:
|
| __init__(self, template)
| Initialize self. See help(type(self)) for accurate signature.
|
| get_identifiers(self)
|
| is_valid(self)
|
| safe_substitute(self, mapping={}, /, **kws)
|
| substitute(self, mapping={}, /, **kws)
|
| ----------------------------------------------------------------------
| Class methods defined here:
|
| __init_subclass__()
| This method is called when a class is subclassed.
|
| The default implementation does nothing. It may be
| overridden to extend subclasses.
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables
|
| __weakref__
| list of weak references to the object
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| braceidpattern = None
|
| delimiter = '$'
|
| flags = re.IGNORECASE
|
| idpattern = '(?a:[_a-z][_a-z0-9]*)'
|
| pattern = re.compile('\n \\$(?:\n ...identifie...
FUNCTIONS
capwords(s, sep=None)
capwords(s [,sep]) -> string
Split the argument into words using split, capitalize each
word using capitalize, and join the capitalized words using
join. If the optional second argument sep is absent or None,
runs of whitespace characters are replaced by a single space
and leading and trailing whitespace are removed, otherwise
sep is used to split and join the words.
DATA
__all__ = ['ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'cap...
ascii_letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
digits = '0123456789'
hexdigits = '0123456789abcdefABCDEF'
octdigits = '01234567'
printable = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU...
punctuation = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
whitespace = ' \t\n\r\x0b\x0c'
FILE
c:\tools\python\lib\string.py
Il arrive assez souvent à un développeur d'avoir besoin d'une certaine fonction dont il a oublié le nom, mais dont il sait qu'elle figure dans un module donné. Dans ce cas de figure, on peut obtenir la liste de tous les identificateurs définis dans un module par la fonction dir(nom_du_module). Comme précédemment, il faut que le module correspondant ait été importé au préalable :
dir(string) # on liste tous les identificateurs définis dans le module 'string'
['Formatter', 'Template', '_ChainMap', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_re', '_sentinel_dict', '_string', 'ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace']
L'information fournie par la fonction help(nom_du_module) est très détaillée, et peut représenter plusieurs centaines de lignes si le module contient de nombreuses fonctionnalités. A l'inverse, celle fournie par la fonction dir(nom_du_module) est souvent trop laconique et ne permet pas forcément au développeur d'avoir les détails dont il a besoin pour choisir les fonctionnalités à mettre en oeuvre.
Le module tools, utilisé dans cette série de tutos, contient une fonction inspect(nom_du_module, detail=0) qui offre une solution plus flexible grâce à un paramètre detail permettant de contrôler la quantité de détails à afficher sur le rôle des éléments fournis par un module. Voici ce que cela donne en l'appliquant sur le module string utilisé ci-dessus :
from SRC.tools import inspect
# l'option 'detail=0' affiche simplement les noms des constantes et des fonctions du module
inspect(string, detail=0)
#inspect(string) # donne la même chose, car 0 est la valeur par défaut de 'detail'
● NAME = string / TYPE = module ● ROLE = A collection of string constants. ● TYPES : use 'inspect(string.xxx)' to get additional info for each inner type Formatter Template ● CONSTANTS ascii_letters ascii_lowercase ascii_uppercase digits hexdigits octdigits printable punctuation whitespace ● FUNCTIONS capwords
# l'option 'detail=1' permet d'afficher les valeurs des constantes
# et la première ligne de la docstring pour chaque fonction
inspect(string, detail=1) # inspect(string, 1) donne la même chose
● NAME = string / TYPE = module
● ROLE = A collection of string constants.
● TYPES : use 'inspect(string.xxx)' to get additional info for each inner type
Formatter Template
● CONSTANTS
ascii_letters:str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_lowercase:str = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase:str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
digits:str = '0123456789'
hexdigits:str = '0123456789abcdefABCDEF'
octdigits:str = '01234567'
printable:str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
punctuation:str = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
whitespace:str = ' \t\n\r\x0b\x0c'
● FUNCTIONS
capwords : Split the argument into words using split, capitalize each...
# l'option 'detail=2' rajoute le premier bloc de chaque docstring
inspect(string, detail=2)
● NAME = string / TYPE = module
● ROLE = A collection of string constants.
● TYPES : use 'inspect(string.xxx)' to get additional info for each inner type
Formatter Template
● CONSTANTS
ascii_letters:str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_lowercase:str = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase:str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
digits:str = '0123456789'
hexdigits:str = '0123456789abcdefABCDEF'
octdigits:str = '01234567'
printable:str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
punctuation:str = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
whitespace:str = ' \t\n\r\x0b\x0c'
● FUNCTIONS
capwords : Split the argument into words using split, capitalize each
word using capitalize, and join the capitalized words using
join. If the optional second argument sep is absent or None,
runs of whitespace characters are replaced by a single space
and leading and trailing whitespace are removed, otherwise
sep is used to split and join the words.
# enfin, l'option 'detail=3' affiche l'intégralité de toutes les docstrings
inspect(string, detail=3)
● NAME = string / TYPE = module
● ROLE = A collection of string constants.
● TYPES : use 'inspect(string.xxx)' to get additional info for each inner type
Formatter Template
● CONSTANTS
ascii_letters:str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
ascii_lowercase:str = 'abcdefghijklmnopqrstuvwxyz'
ascii_uppercase:str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
digits:str = '0123456789'
hexdigits:str = '0123456789abcdefABCDEF'
octdigits:str = '01234567'
printable:str = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
punctuation:str = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
whitespace:str = ' \t\n\r\x0b\x0c'
● FUNCTIONS
capwords : capwords(s [,sep]) -> string
Split the argument into words using split, capitalize each
word using capitalize, and join the capitalized words using
join. If the optional second argument sep is absent or None,
runs of whitespace characters are replaced by a single space
and leading and trailing whitespace are removed, otherwise
sep is used to split and join the words.
2 - Principaux modules de la bibliothèque standard¶
Pour finir ce chapitre de synthèse sur le langage Python, voici une liste triée par thématique des principaux modules de la bibliothèque standard qui sont fréquemment utilisés dans le domaine des Sciences des Données. Il est conseillé de faire un inspect(nom_du_module, detail=2) sur chacun d'eux pour avoir un résumé des fonctionnalités qu'apporte chaque module. Certains de ces modules seront étudiés plus en détail durant les séances de TD, pour les autres vous avez accès leur documentation complète, soit sur le site de Python, soit directement dans l'environnement JupyterLab par le menu Python Reference de l'onglet Help sur la barre de menus :
- Traitements numériques :
math●cmath●statistics●decimal●fractions●random - Traitements itératifs :
itertools●re - Manipulation de données structurées en mémoire :
collections●dataclasses●datetime - Manipulation de données structurées sur fichiers :
pickle●csv●json●xml●toml●sqlite3●urllib
base64●gzip●lzma●zipfile●tarfile - Interaction avec le système d'exploitation :
sys●os●pathlib●glob●shutil●webbrowser
Voici, par exemple, un résumé du contenu du module math qui a déjà été mentionné plus haut dans le chapitre :
import math
inspect(math, 1) # on peut mettre directement le niveau de détail souhaité, sans mettre 'detail=1'
● NAME = math / TYPE = module ● ROLE = This module provides access to the mathematical functions defined by the C standard. ● CONSTANTS e:float = 2.718281828459045 inf:float = inf nan:float = nan pi:float = 3.141592653589793 tau:float = 6.283185307179586 ● FUNCTIONS acos : Return the arc cosine (measured in radians) of x. acosh : Return the inverse hyperbolic cosine of x. asin : Return the arc sine (measured in radians) of x. asinh : Return the inverse hyperbolic sine of x. atan : Return the arc tangent (measured in radians) of x. atan2 : Return the arc tangent (measured in radians) of y/x. atanh : Return the inverse hyperbolic tangent of x. cbrt : Return the cube root of x. ceil : Return the ceiling of x as an Integral. comb : Number of ways to choose k items from n items without repetition and without order. copysign : Return a float with the magnitude (absolute value) of x but the sign of y. cos : Return the cosine of x (measured in radians). cosh : Return the hyperbolic cosine of x. degrees : Convert angle x from radians to degrees. dist : Return the Euclidean distance between two points p and q. erf : Error function at x. erfc : Complementary error function at x. exp : Return e raised to the power of x. exp2 : Return 2 raised to the power of x. expm1 : Return exp(x)-1. fabs : Return the absolute value of the float x. factorial : Find n!. floor : Return the floor of x as an Integral. fmod : Return fmod(x, y), according to platform C. frexp : Return the mantissa and exponent of x, as pair (m, e). fsum : Return an accurate floating point sum of values in the iterable seq. gamma : Gamma function at x. gcd : Greatest Common Divisor. hypot : Multidimensional Euclidean distance from the origin to a point. isclose : Determine whether two floating point numbers are close in value. isfinite : Return True if x is neither an infinity nor a NaN, and False otherwise. isinf : Return True if x is a positive or negative infinity, and False otherwise. isnan : Return True if x is a NaN (not a number), and False otherwise. isqrt : Return the integer part of the square root of the input. lcm : Least Common Multiple. ldexp : Return x * (2**i). lgamma : Natural logarithm of absolute value of Gamma function at x. log : If the base is not specified, returns the natural logarithm (base e) of x. log10 : Return the base 10 logarithm of x. log1p : Return the natural logarithm of 1+x (base e). log2 : Return the base 2 logarithm of x. modf : Return the fractional and integer parts of x. nextafter : Return the floating-point value the given number of steps after x towards y. perm : Number of ways to choose k items from n items without repetition and with order. pow : Return x**y (x to the power of y). prod : Calculate the product of all the elements in the input iterable. radians : Convert angle x from degrees to radians. remainder : Difference between x and the closest integer multiple of y. sin : Return the sine of x (measured in radians). sinh : Return the hyperbolic sine of x. sqrt : Return the square root of x. sumprod : Return the sum of products of values from two iterables p and q. tan : Return the tangent of x (measured in radians). tanh : Return the hyperbolic tangent of x. trunc : Truncates the Real x to the nearest Integral toward 0. ulp : Return the value of the least significant bit of the float x.