J’ai déjà évoqué à plusieurs reprises la bibliothèque standard de Python (ou stdlib pour standard library), il s’agit de l’ensemble des modules et fonctions qui sont inclus de base avec Python.
Chaque langage vient avec sa bibliothèque standard mais celle de Python est particulièrement fournie.
Nous en avons déjà parcouru une partie au cours des chapitres précédents et je ne pourrai pas être exhaustif ici non plus. Sachez qu’elle comprend par exemple des modules pour gérer les nombres fractionnaires, les dates, les chemins de fichier, mais aussi les archives ZIP, les emails, le protocole HTTP, etc.
Un réflexe à avoir lorsque vous recherchez une fonctionnalité particulière en Python et de d’abord regarder si celle-ci est présente dans un module de la bibliothèque standard, et pour cela la documentation est votre amie.
Fonctions natives
Je ne reviendrai pas sur l’ensemble des fonctions natives (built-ins) car beaucoup ont déjà été présentées dans les chapitres précédents, notamment celui rappelant les différents types et celui dédié aux outils sur les boucles.
Mais quelques autres de ces fonctions méritent qu’on en parle un peu.
Manipulation de caractères
Les fonctions ord
et chr
par exemple permettent de manipuler les caractères et leurs codes numériques.
Jusqu’ici on n’a jamais dissocié caractères et chaînes de caractères, puisque les caractères sont simplement des chaînes de taille 1.
Mais en pratique, une chaîne de caractères s’apparente plutôt à une séquence de code numériques (des nombres entiers) où chaque code identifie un caractère particulier selon la spécification unicode.
Ainsi, la fonction ord
permet simplement de récupérer le code associé à un caractère, et la fonction chr
le caractère associé à un code.
>>> ord('x')
120
>>> chr(120)
'x'
>>> ord('♫')
9835
>>> chr(9835)
'♫'
Ces fonctions peuvent permettre de jongler un peu avec la table unicode pour réaliser des opérations particulières en exploitant les caractéristiques de cette table.
Par exemple pour récupérer n’importe quelle carte à jouer en connaissant la manière dont elles sont stockées :
>>> card_base = ord('🂠')
>>> chr(card_base + 0x20 + 0x05) # 5 de carreau
'🃅'
>>> chr(card_base + 0x10 + 0x0B) # Valet de pic
'🂻'
ord
échoue naturellement si on lui passe une chaîne de plusieurs caractères, et chr
si on lui donne un code en dehors des bornes définies par unicode.
>>> ord('salut')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ord() expected a character, but string of length 5 found
>>> chr(1000000000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: chr() arg not in range(0x110000)
Formattage des valeurs
La fonction format
permet d’obtenir la représentation formatée de n’importe quelle valeur, sous forme d’une chaîne de caractères.
Vous ne la connaissez pas mais c’est elle qui intervient dans le mécanisme des chaînes de formatage (f-string) pour transformer les valeurs et leur appliquer le format demandé.
Elle prend ainsi en arguments la valeur et le format (les options de formatage) à lui appliquer.
>>> format(42, '05X')
'0002A'
>>> format(123.4, 'e')
'1.234000e+02'
>>> format('salut', '>10')
' salut'
Appelée sans format, elle opère juste la conversion en chaîne de caractères de la valeur donnée et devient ainsi équivalente à str
.
>>> format(25)
'25'
Évaluation dynamique
La fonction qui suit peut introduire de grosses failles de sécurité dans vos programmes et doit donc être utilisée avec parcimonie : seulement sur des données qui sont sûres, jamais sur des données reçues de l’utilisateur ou d’un tiers.
Python est un langage dynamique et permet en cela d’exécuter du code à la volée au sein du programme.
C’est l’objectif de la fonction eval
qui prend en argument une chaîne de caractères représentant une expression Python, l’interprète et en renvoie le résultat.
>>> eval('1 + 3')
4
>>> x = 5
>>> eval('x * 8')
40
Cela offre donc la possibilité d’exécuter du code dynamiquement et donc de dépasser les fonctionnalités de base du langage. Par exemple pour créer en un coup une imbrication de 20 listes.
>>> eval('['*20 + 'None' + ']'*20)
[[[[[[[[[[[[[[[[[[[[None]]]]]]]]]]]]]]]]]]]]
Toutes ces fonctions natives peuvent être retrouvées sur la page de documentation dédiée.
Module operator
Les opérateurs font en quelque sorte partie des built-ins même si on y pense moins. Après tout, il s’agit aussi de fonctions natives de Python.
Mais les opérateurs sont des symboles et on ne peut pas les manipuler en tant que tels.
En revanche, le module operator
fournit pour chaque opérateur de Python un équivalent sous forme de fonction.
On y trouve ainsi des fonctions add
, sub
, pow
ou encore eq
.
>>> import operator
>>> operator.add(3, 5)
8
>>> operator.sub(10, 1)
9
>>> operator.pow(2, 3)
8
>>> operator.eq('a', 'a')
True
>>> operator.eq('a', 'b')
False
Quelques subtilités à noter :
-
Il y a deux fonctions de division (
truediv
etfloordiv
) pour les deux opérateurs correspondant (respectivement/
et//
).>>> operator.truediv(10, 4) 2.5 >>> operator.floordiv(10, 4) 2
-
operator.concat
(concaténation) est équivalent àoperator.add
, ces deux opérations se représentant par l’opérateur+
, mais s’attend à ce que ses arguments soient des séquences.>>> operator.concat('foo', 'bar') 'foobar' >>> operator.add('foo', 'bar') 'foobar' >>> operator.concat(3, 5) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object can't be concatenated
-
Les opérateurs
&
et|
deviennentand_
etor_
, suffixés d’un_
pour ne pas générer de conflit avec les mots-clésand
etor
. De même quenot
devientnot_
.>>> operator.and_(3, 1) 1 >>> operator.or_(3, 1) 3 >>> operator.not_(False) True
-
Pour chaque fonction
xxx
d’un opérateur arithmétique on trouve une fonctionixxx
pour l’opérateur en-place (par-exempleiadd
pour+=
).>>> values = [] >>> operator.iadd(values, [42]) [42] >>> values [42]
-
Les opérateurs
foo[key]
,foo[key] = value
etdel foo[key]
sont appelésgetitem
,setitem
etdelitem
.getitem
renvoie la valeur demandée,setitem
etdelitem
renvoientNone
.>>> operator.setitem(values, 0, 21) >>> operator.getitem(values, 0) 21 >>> operator.delitem(values, 0) >>> values []
-
On trouve une fonction spéciale
itemgetter
qui permet de générer un opérateur renvoyant la valeur associée à une clé dans un conteneur.>>> get_3rd = operator.itemgetter(3) >>> get_3rd('abcdef') 'd' >>> get_3rd([3, 4, 5, 6]) 6 >>> get_3rd(range(10)) 3 >>> get_foo = operator.itemgetter('foo') >>> get_foo({'foo': -12}) -12
Gestion des nombres
On a vite fait de passer sur les nombres, car on peut croire que l’on en a fait le tour une fois que l’on a vu les types int
, float
et complex
qui semblent en effet couvrir toutes les catégories de nombres que l’on connaît.
Si c’est vrai pour ce qui est des nombres entiers, les types dédiés aux réels et aux complexes n’en sont que des approximations.
Nombres décimaux
En effet, nous avons vu que les float
étaient stockés par l’ordinateur sous forme binaire et étaient donc souvent des approximations des nombres décimaux que nous connaissons, ce qui pouvait mener à des erreurs d’arrondis.
>>> 0.1 + 0.1 + 0.1
0.30000000000000004
Un autre type existe néanmoins pour représenter de façon précise un nombre décimal, il s’agit du type Decimal
du module decimal
.
>>> from decimal import Decimal
>>> Decimal('0.1')
Decimal('0.1')
>>> Decimal('0.1') + Decimal('0.1') + Decimal('0.1')
Decimal('0.3')
Un Decimal
s’instancie avec une chaîne de caractère représentant notre nombre décimal et se comporte ensuite comme n’importe quel nombre : toutes les opérations usuelles peuvent s’y appliquer.
>>> Decimal('1.5') * Decimal('3.7')
Decimal('5.55')
>>> Decimal('-4.2') - Decimal('1.1')
Decimal('-5.3')
Les décimaux sont aussi compatibles avec les entiers, une opération entre des nombres des deux types renverra toujours un décimal.
>>> Decimal('0.1') + 3
Decimal('3.1')
>>> Decimal('0.1') * 4
Decimal('0.4')
>>> Decimal('0.1') ** 2
Decimal('0.01')
Vous vous demandez pourquoi on instancie un Decimal
avec une chaîne de caractères plutôt qu’un float
?
Il est en fait possible de créer un décimal à partir d’un flottant, mais ce flottant comprenant dès le départ une erreur d’arrondi, celle-ci se répercutera sur le décimal.
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')
On notera par ailleurs qu’il est possible de créer un décimal à partir d’un nombre entier, ce dernier ne comportant pas d’approximation.
>>> Decimal(1) / Decimal(10)
Decimal('0.1')
À tout moment, il est possible de converir un décimal en entier ou flottant à l’aide d’un appel à int
ou float
.
>>> int(Decimal('1.4') * Decimal('1.5'))
2
>>> float(Decimal('1.4') * Decimal('1.5'))
2.1
Les nombres décimaux sont pratiques pour manipuler des valeurs qui ne doivent pas subir d’arrondis involontaires, comme des valeurs monétaires, mais sont moins performants à manipuler que les flottants qui sont directement gérés par le processeur.
Enfin, les décimaux sont tout de même soumis à une précision limitée qui ne leur permet alors pas de représenter tous les nombres décimaux possibles.
Avec une précision par défaut de 28 décimales, on remarque ainsi qu’il y a une perte de précision quand il y a une trop grande distance entre le chiffre le plus à gauche et celui le plus à droite, qui se ressent lors des opérations suivantes.
>>> Decimal('1.000000000000000000000000001') * 2
Decimal('2.000000000000000000000000002')
>>> Decimal('1.0000000000000000000000000001') * 2
Decimal('2.000000000000000000000000000')
La précision des décimaux peut cependant être connue et réglée à l’aide des fonctions getcontext
et setcontext
tel que décrit dans la documentation du module.
>>> from decimal import getcontext
>>> ctx = getcontext()
>>> ctx.prec = 30
>>> Decimal('1.0000000000000000000000000001') * 2
Decimal('2.0000000000000000000000000002')
>>> ctx.prec = 1
>>> Decimal('1.01') + Decimal('1.01')
Decimal('2')
Nombres rationnels
Mais quelle que soit la précision choisie, celle-ci sera toujours finie (pour des raisons de performances), et on ne pourra donc pas représenter un nombre avec une infinité ou un trop grand nombre de décimales.
On ne peut pas représenter de façon exacte le nombre avec un Decimal
.
En revanche, il existe un autre type pour représenter les nombres rationnels : le type Fraction
du module fractions
.
Pour rappel, un nombre rationnel est un nombre qui peut s’écrire comme le quotient (la fraction) entre deux nombres entiers, tels que (1/3
), (1.5
) ou encore (4
).
Un objet Fraction
s’instancie avec le numérateur et le dénominateur de la fraction et s’utilise ensuite comme n’importe quel nombre.
>>> from fractions import Fraction
>>> Fraction(1, 3) + Fraction(1, 3)
Fraction(2, 3)
>>> Fraction(1, 3) * Fraction(3, 1)
Fraction(1, 1)
Les fractions sont elles aussi compatibles avec les entiers, et convertibles en int
ou float
.
>>> Fraction(1, 3) * 4
Fraction(4, 3)
>>> int(Fraction(4, 3))
1
>>> float(Fraction(4, 3))
1.3333333333333333
Ce type offre donc une précision infinie pour les nombres rationnels, mais avec un certain coût en performances. Ne l’utilisez donc que si vous avez besoin d’une précision exacte sur vos nombres, comme pour un solveur d’équations.
Hiérarchie des nombres
Les types de nombres sont généralement compatibles entre eux : il est possible d’exécuter une opération entre un entier et un flottant, comme entre un rationnel et un complexe. Mais qu’attendre du résultat d’une telle opération ?
On le sait, une opération entre un entier et un flottant renvoie un flottant, car c’est lui qui est le plus à même de stocker le résultat.
En effet, 2 * 3.4
ne pourra pas être représenté dans un entier.
Il existe en fait une hiérarchie entre les types numériques qui définit quel type doit être renvoyé lors d’une telle opération. Il s’agira toujours du type le plus haut dans la hiérarchie.
Cette hiérarchie reprend les notions d’ensembles de nombres en mathématiques : il y a les nombres complexes tout en haut, puis les réels, les rationnels, les relatifs et enfin les entiers naturels.
En Python, les complexes sont représentés par le type complex
, les réels par float
, les rationnels par fractions.Fraction
, et les entiers relatifs et naturels par int
.
Cela explique qu’une opération entre une fraction et un complexe renverra toujours un complexe.
>>> Fraction(1, 3) + 2j
(0.3333333333333333+2j)
Mais ce ne sont pas les seuls types de nombres que vous pourriez être amenés à manipuler, et certaines bibliothèques pourraient venir avec leurs propres types.
Pour autant, ces types se conformeraient à la hiérarchie présentée au-dessus car ils y feraient référence en utilisant les types abstraits (Number
— nombre, Complex
— complexe, Real
— réel, Rational
— rationnel, Integral
— entier) définis dans le module numbers
.
Les types abstraits ainsi définis permettent de savoir à quelle classe appartient à un nombre, à l’aide d’appels à isinstance
.
>>> import numbers
>>> isinstance(4, numbers.Integral) # Les int sont des entiers
True
>>> isinstance(4, numbers.Real) # Mais ce sont aussi des réels
True
>>> isinstance(4, numbers.Number) # Ou tout simplement des nombres au sens large
True
>>> isinstance(4.2, numbers.Real) # Les flottants sont des réels
True
>>> isinstance(4.2, numbers.Rational) # Mais ne sont pas des rationnels
False
>>> isinstance(Fraction(1, 3), numbers.Rational) # Contrairement aux fractions
True
Les décimaux sont un cas un peu à part car ils ne s’inscrivent pas dans la hiérarchie, ils se situent quelque part entre les entiers et les rationnels. Aussi, les décimaux ne sont pas considérés comme des instances de numbers.Real
ou numbers.Rational
.
>>> isinstance(Decimal('0.1'), numbers.Number)
True
>>> isinstance(Decimal('0.1'), numbers.Real)
False
>>> isinstance(Decimal('0.1'), numbers.Rational)
False
Cela implique que les décimaux ne sont pas compatibles avec les autres types de la hiérarchie, et ne le sont en fait qu’avec les entiers.
Bibliothèques mathématiques
On a vu qu’il existait en Python le module math
qui regroupe l’essentiel des fonctions mathématiques sur les nombres réels, et dont on peut retrouver la liste sur la page de documentation dédiée.
On le sait moins, mais il existe aussi un module cmath
pour des fonctions équivalentes dans le domaine des complexes.
>>> import math
>>> math.sqrt(2) # Racine carrée de 2
1.4142135623730951
>>> math.sqrt(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> import cmath
>>> cmath.sqrt(2)
(1.4142135623730951+0j)
>>> cmath.sqrt(-1)
1j
Le module étend donc le domaine de définition de certaines fonctions de math
pour permettre de les appliquer à des nombres complexes. C’est le cas des fonctions trigonométriques ou exponentielles par exemple.
>>> cmath.cos(1+2j)
(2.0327230070196656-3.0518977991518j)
>>> cmath.exp(1j * cmath.pi)
(-1+1.2246467991473532e-16j)
Toutes ces fonctions sont à retrouver dans la documentation du module cmath
.
Mais à propos de nombres, on trouve aussi le module statistics
qui comme son nom l’indique fournit des outils de statistiques.
On trouvera ainsi des fonctions pour calculer la moyenne (mean
), la médiane (median
), la variance (variance
) ou encore l’écart type (stdev
) d’une série de données.
>>> data = [1, 2, 2, 3, 4, 5, 5, 6, 7]
>>> statistics.mean(data)
3.888888888888889
>>> statistics.median(data)
4
>>> statistics.variance(data)
4.111111111111111
>>> statistics.stdev(data)
2.0275875100994063
Pour plus d’informations sur les outils fournis par ce module, je vous invite à vous reporter sur sa documentation.
Chemins et fichiers
On a déjà croisé la pathlib
plus tôt pour tester l’existence d’un fichier.
Mais ce module de la bibliothèque standard va bien au-delà et propose une ribambelle d’outils pour travailler avec les chemins et les fichiers.
Le module pathlib
définit principalement le type Path
ainsi que d’autres types qui en dépendent suivant l’implémentation.
Ainsi, quand vous instanciez un objet Path
vous obtiendrez une instance d’un autre type suivant votre système d’exploitation (WindowsPath
pour Windows et PosixPath
pour les autres systèmes).
>>> from pathlib import Path
>>> Path()
PosixPath('.')
Usage des chemins
On le voit, on peut instancier un Path
sans argument, le chemin correspond alors au répertoire courant (c’est ce que signifie le point).
Mais on peut aussi passer un chemin (relatif comme absolu) en argument pour obtenir un objet Path
correspondant.
>>> Path('/')
PosixPath('/')
>>> Path('../subdir/file.py')
PosixPath('../subdir/file.py')
L’intérêt des objets Path
est qu’ils sont composables entre eux, à l’aide de l’opérateur /
(qui représente la séparation entre répertoires).
>>> Path('a') / Path('b') / Path('c')
PosixPath('a/b/c')
Un raccourci permet d’ailleurs de composer des chemins directement avec des chaînes de caractères.
>>> Path('a') / 'b'
PosixPath('a/b')
Et les chemins de type Path
sont bien sûr convertibles en chaînes de caractères via un appel explicite à str
, ou en chaîne d’octets avec bytes
>>> path = Path('a')
>>> str(path)
'a'
>>> str(path / 'b')
'a/b'
>>> bytes(path)
b'a'
Ces objets peuvent aussi directement être utilisés pour certaines opérations qui attendent des chemins.
>>> with open(path, 'w') as f:
... f.write('hello')
...
5
Propriétés des chemins
Les objets Path
sont pourvus de nombreux attributs et méthodes et j’aimerais vous en présenter les plus importants.
parts
Premièrement il est possible d’accéder à la décomposition d’un chemin à l’aide de son attribut parts
.
On obtient ainsi un tuple des répertoires / fichiers qui composent notre chemin.
>>> path.parts
('a',)
>>> Path('../subdir/file.py').parts
('..', 'subdir', 'file.py')
name
L’attribut name
renvoie la dernière partie du chemin, soit le nom du fichier cible.
>>> path.name
'a'
>>> Path('../subdir/file.py').name
'file.py'
suffix
, stem
et suffixes
suffix
renvoie le suffixe d’un chemin, plus communément appelé l’extension du fichier.
Si aucune extension n’est présente, suffix
renvoie une chaîne vide.
>>> path.suffix
''
>>> Path('../subdir/file.py').suffix
'.py'
À l’inverse, l’attribut stem
renvoie le nom du fichier dépourvu du suffixe.
>>> path.stem
'a'
>>> Path('../subdir/file.py').stem
'file'
Si un chemin contient plusieurs extensions (.tar.gz
par exemple), seule la dernière extension sera renvoyée par suffix
(et retirée de stem
).
L’attribut suffixes
permet alors de récupérer la liste de toutes les extensions.
>>> Path('photos.tar.gz').suffix
'.gz'
>>> Path('photos.tar.gz').stem
'photos.tar'
>>> Path('photos.tar.gz').suffixes
['.tar', '.gz']
parent
et parents
On peut accéder au parent d’un chemin (son répertoire parent) via l’attribut parent
.
parent
est en quelque sort l’inverse de name
.
>>> path.parent
PosixPath('.')
>>> Path('../subdir/file.py').parent
PosixPath('../subdir')
L’attribut parents
permet aussi d’accéder à l’ensemble des parents d’un chemin.
path.parents[0]
correspondra ainsi à path.parent
, path.parents[1]
à path.parent.parent
, etc.
>>> Path('../subdir/file.py').parents[0]
PosixPath('../subdir')
>>> Path('../subdir/file.py').parents[1]
PosixPath('..')
>>> Path('../subdir/file.py').parents[2]
PosixPath('.')
Attention, l’attribut parents
ne renvoie pas une liste mais un type particulier de séquence.
On peut bien sûr le convertir en liste avec un appel à list
.
>>> Path('../subdir/file.py').parents
<PosixPath.parents>
>>> list(Path('../subdir/file.py').parents)
[PosixPath('../subdir'), PosixPath('..'), PosixPath('.')]
is_absolute
La méthode is_absolute
est un prédicat pour savoir si un chemin est absolu (débute par la racine du système de fichiers) ou non.
>>> Path('dir/hello.txt').is_absolute()
False
>>> Path('/home/antoine/dir/hello.txt').is_absolute()
True
relative_to
La méthode relative_to
permet de convertir un chemin pour l’obtenir relativement à un autre.
Elle offre ainsi un moyen de convertir un chemin absolu vers un chemin relatif.
Par exemple le chemin /home/antoine/dir/hello.txt
donne dir/hello.txt
relativement à /home/antoine
.
>>> Path('/home/antoine/dir/hello.txt').relative_to('/home/antoine')
PosixPath('dir/hello.txt')
Mais on peut aussi le calculer à partir de chemins relatifs.
>>> Path('dir/hello.txt').relative_to('dir')
PosixPath('hello.txt')
Dans le cas où aucune correspondance n’est trouvée et qu’il n’est donc pas possible de construire un chemin relatif entre les deux, la méthode lève une exception ValueError
.
De même si on mélange chemins absolus et relatifs.
>>> Path('dir/hello.txt').relative_to('dir2')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.9/pathlib.py", line 939, in relative_to
raise ValueError("{!r} is not in the subpath of {!r}"
ValueError: 'dir/hello.txt' is not in the subpath of 'dir2' OR one path is relative and the other is absolute.
Méthodes concrètes
Toutes les méthodes précédentes permettent de manipuler les chemins de façon abstraite, déconnectée du système de fichiers. Mais d’autres méthodes servent à réaliser des opérations concrètes en s’appuyant sur le système.
exists
Nous l’avons déjà rencontrée, la méthode exists
est un prédicat pour tester si le chemin pointe vers un fichier/répertoire qui existe ou non.
>>> Path('notfound').exists()
False
>>> Path('hello.txt').exists()
True
>>> Path('/').exists()
True
is_dir
et is_file
Les méthodes is_dir
et is_file
permettent respectivement de tester si un chemin pointe vers un répertoire ou vers un fichier.
>>> Path('hello.txt').is_dir()
False
>>> Path('hello.txt').is_file()
True
>>> Path('/').is_dir()
True
>>> Path('/').is_file()
False
Ces méthodes renvoient False
quand le chemin n’existe pas.
>>> Path('notfound').is_dir()
False
>>> Path('notfound').is_file()
False
resolve
La méthode resolve
permet de résoudre un chemin, soit de trouver le chemin absolu correspondant.
>>> path.resolve()
PosixPath('/home/antoine/a')
>>> Path('hello.txt').resolve()
PosixPath('/home/antoine/hello.txt')
>>> Path('../subdir/file.py').resolve()
PosixPath('/home/subdir/file.py')
>>> Path('/').resolve()
PosixPath('/')
Elle peut s’utiliser avec un argument strict
pour lever une erreur si le chemin en question n’existe pas.
>>> Path('notfound').resolve()
PosixPath('/home/antoine/notfound')
>>> Path('hello.txt').resolve(strict=True)
PosixPath('/home/antoine/hello.txt')
>>> Path('notfound').resolve(strict=True)
Traceback (most recent call last):
[...]
FileNotFoundError: [Errno 2] No such file or directory: '/home/antoine/notfound'
cwd
cwd
est une méthode du type Path
, qui renvoie le chemin vers le répertoire courant.
C’est ce chemin qui est utilisé pour les résolutions de resolve
.
>>> Path.cwd()
PosixPath('/home/antoine')
Méthodes pour les répertoires
Certaines méthodes sont spécifiques aux chemins représentant des répertoires.
mkdir
et rmdir
La méthode mkdir
permet de créer un répertoire là où pointe le chemin.
>>> Path('subdir').exists()
False
>>> Path('subdir').mkdir()
>>> Path('subdir').exists()
True
>>> Path('subdir').is_dir()
True
La méthode lève une erreur FileExistsError
si le répertoire (ou un fichier) existe déjà à ce chemin.
À l’inverse, la méthode rmdir
permet de supprimer le répertoire pointé, et lève une erreur FileNotFoundError
s’il n’existe pas.
>>> Path('subdir').rmdir()
>>> Path('subdir').rmdir()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.9/pathlib.py", line 1363, in rmdir
self._accessor.rmdir(self)
FileNotFoundError: [Errno 2] No such file or directory: 'subdir'
Un répertoire doit être vide pour pouvoir être supprimé par rmdir
.
iterdir
Le principe des répertoires, c’est de contenir des fichiers.
Ainsi les chemins possèdent une méthode iterdir
qui renvoie un itérable pour parcourir les fichiers contenus dans le dossier.
>>> for p in Path('.').iterdir():
... print(p)
...
hello.txt
subdir
game.py
Comme on le voit, les fichiers que l’on obtient ne sont pas particulièrement triés.
On peut toujours faire appel à sorted
si cela est nécessaire.
Le parcours n’est pas récursif, les fichiers contenus dans les sous-dossiers (subdir
par exemple) ne sont donc pas explorés.
glob
glob
est une autre méthode pour explorer les fichiers présents dans un dossier, qui permet de les rechercher selon un critère.
En effet, glob
prend une chaîne de caractères en argument qui décrit quels fichiers rechercher dans le répertoire.
Cette chaîne doit correspondre à un nom de fichier mais peut comprendre des *
qui agissent comme des jokers et correspondent à n’importe quels caractères. Ainsi, *.py
permet de trouver tous les fichiers .py
d’un répertoire, et glob('*')
est équivalent à iterdir()
.
>>> for p in Path('.').glob('*.py'):
... print(p)
...
game.py
Méthodes pour les fichiers
Certaines autres méthodes sont spécifiques aux fichiers.
touch
et unlink
touch
est la méthode qui permet de créer le fichier pointé par le chemin.
>>> Path('newfile.txt').exists()
False
>>> Path('newfile.txt').touch()
>>> Path('newfile.txt').exists()
True
La méthode ne produit pas d’erreur si le fichier existe déjà (met elle modifiera sa date de dernière modification).
>>> Path('newfile.txt').touch()
Et dans l’autre sens, on trouve la méthod unlink
pour supprimer un fichier.
>>> Path('newfile.txt').unlink()
>>> Path('newfile.txt').exists()
False
La méthode lève une exception FileNotFoundError
si le fichier n’existe pas, mais depuis Python 3.8 il est possible de lui préciser un argument booléen missing_ok
pour ne pas produire d’erreur.
>>> Path('newfile.txt').unlink()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.9/pathlib.py", line 1354, in unlink
self._accessor.unlink(self)
FileNotFoundError: [Errno 2] No such file or directory: 'newfile.txt'
>>> Path('newfile.txt').unlink(missing_ok=True)
open
Nous l’avons déjà vu, la méthode open
est semblable à la fonction built-in open
, sauf qu’elle s’applique à un chemin.
La méthode prend alors un mode en argument, 'r'
par défaut, et renvoie un gestionnaire de contexte sur le fichier.
>>> with path.open('w') as f_out:
... f_out.write('coucou')
...
6
>>> with path.open() as f_in:
... print(f_in.read())
...
coucou
read_text
et read_bytes
Pour simplifier certaines opérations, il existe aussi des méthodes read_text
et read_bytes
pour lire dans une chaîne le contenu d’un fichier.
read_text
renvoie une chaîne de caractères et read_bytes
une chaîne d’octets.
>>> path.read_text()
'coucou'
>>> path.read_bytes()
b'coucou'
write_text
et write_bytes
Et réciproquement, les méthodes write_text
et write_bytes
permettent de remplacer le contenu d’un fichier par la chaîne donnée en argument.
>>> path.write_text('bonne soirée')
12
>>> path.read_text()
'bonne soirée'
>>> path.write_bytes(b'\x01\x02\x03')
3
>>> path.read_bytes()
b'\x01\x02\x03'
Le fichier est automatiquement créé s’il n’existe pas.
>>> Path('notfound').write_text('abc')
3
>>> Path('notfound').read_text()
'abc'
Toutes les autres méthodes des objets Path
sont à découvrir sur la page de documentation du module pathlib
.
Modules systèmes
Python dispose aussi de modules pour interagir avec le système, notamment les modules sys
, shutil
et os
.
Module sys
sys
est un module qui fournit différentes informations sur le système d’exploitation et l’interpréteur Python.
On trouve notamment des attributs platform
, version
et version_info
pour connaître l’OS utilisé et la version de Python.
>>> import sys
>>> sys.platform
'linux'
>>> sys.version
'3.9.7 (default, Aug 31 2021, 13:28:12) \n[GCC 11.1.0]'
>>> sys.version_info
sys.version_info(major=3, minor=9, micro=7, releaselevel='final', serial=0)
>>> sys.version_info.major, sys.version_info.minor
(3, 9)
On peut aussi accéder au chemin de l’exécutable Python (executable
), ainsi qu’à la liste des arguments du programme (argv
).
>>> sys.executable
'/usr/bin/python'
>>> sys.argv
['']
Le module met à disposition les fichiers stdin
, stdout
et stderr
qui sont liés respectivement à l’entrée standard, la sortie standard et la sortie d’erreur.
>>> sys.stdin.readline()
hello
'hello\n'
>>> sys.stdout.write('coucou\n')
coucou
7
>>> sys.stderr.write('error\n')
error
6
Le dictionnaire modules
référence tous les modules importés au sein de l’interpréteur.
C’est un mécanisme de cache au sein de Python pour éviter de charger plusieurs fois un même module.
>>> sys.modules
{'sys': <module 'sys' (built-in)>, 'builtins': <module 'builtins' (built-in)>, ...}
>>> sys.modules['sys']
<module 'sys' (built-in)>
>>> sys.modules['sys'].platform
'linux'
Quand je vous parlais de récursivité, j’évoquais une limite au nombre de récursions autorisées par l’interpréteur Python.
Cette limite peut être connue via un appel à la fonction getrecursionlimit
du module sys
.
>>> sys.getrecursionlimit()
1000
Enfin, nous l’avons déjà rencontrée, la fonction exit
permet de couper le programme en cours d’exécution.
Utilisée sans argument, la fonction coupe le programme normalement avec un code de retour de 0 (signifiant que tout s’est bien passé).
>>> sys.exit()
% echo $?
0
Avec un nombre en argument, c’est ce nombre qui sera utilisé comme code de retour (un code de retour différent de 0 signifie que le programme s’est terminé sur une erreur).
>>> sys.exit(12)
% echo $?
12
Avec une chaîne de caractères en argument, la chaîne sera écrite sur la sortie d’erreur et le code de retour sera 1.
>>> sys.exit('error')
error
% echo $?
1
L’ensemble de ces fonctions, et bien d’autres encore, peut être retrouvé sur la page de documentation du module sys
.
Module shutil
shutil
peut venir en complément de pathlib
, il propose des opérations de haut-niveau sur les fichiers et répertoires, notamment pour les copies, les déplacements et la suppression.
On trouve ainsi une fonction copy
qui permet de copier un fichier à un autre endroit sur le système.
La fonction prend en arguments le chemin source et sa destination, les chaînes de caractères et les objets Path
sont acceptés.
Elle renvoie le chemin du fichier copié.
>>> from pathlib import Path
>>> import shutil
>>> shutil.copy(Path('hello.txt'), 'new.txt')
'new.txt'
>>> Path('new.txt').read_text()
'salut\n'
Il est aussi possible de préciser un répertoire en second argument pour copier le fichier (en conservant son nom) vers ce répertoire.
>>> shutil.copy('hello.txt', 'subdir')
'subdir/hello.txt'
>>> Path('subdir/hello.txt').read_text()
'salut\n'
Pour copier des arborescences de fichiers (fichiers et répertoires), shutil
propose une fonction copytree
sur le même principe que copy
.
La fonction copie récursivement le répertoire source et les fichiers qu’il contient vers la destination.
>>> shutil.copytree('subdir', 'newdir')
'newdir'
>>> list(Path('newdir').iterdir())
[PosixPath('newdir/hello.txt'), PosixPath('newdir/file.py')]
De la même manière, on trouve une fonction move
pour déplacer un fichier ou un répertoire vers une destination.
>>> shutil.move('new.txt', 'moved.txt')
'moved.txt'
>>> Path('moved.txt').read_text()
'salut\n'
>>> Path('new.txt').exists()
False
>>> shutil.move('newdir', 'moveddir')
'moveddir'
>>> list(Path('moveddir').iterdir())
[PosixPath('moveddir/hello.txt'), PosixPath('moveddir/file.py')]
>>> Path('newdir').exists()
False
Et le module offre aussi une fonction rmtree
pour supprimer récursivement un répertoire.
>>> shutil.rmtree('moveddir')
>>> Path('moveddir').exists()
False
Enfin, dans un tout autre genre, la fonction get_terminal_size
permet de connaître la taille (en lignes de caractères et en colonnes) du terminal.
La fonction renvoie un tuple nommé avec deux champs columns
et lines
.
>>> shutil.get_terminal_size()
os.terminal_size(columns=136, lines=66)
La page de documentation de shutil
complètera les informations au sujet de ce module.
Module os
os
est l’interface bas-niveau du système d’exploitation (os pour operating system), le module offre une multitude de fonctions pour communiquer avec lui.
On trouve notamment des fonctions pour manipuler les fichiers et répertoires telles que mkdir
, rmdir
, unlink
, open
, etc.
Ces fonctions sont celles qui sont utilisées par la pathlib
qui leur ajoute une interface plus haut-niveau pour manipuler ces données.
La plupart des fonctions exposées dans os
sont d’ailleurs abstraites dans d’autres modules (subprocess
, shutil
) pour les rendre plus faciles à utiliser.
De la même manière, on trouve le module os.path
, antérieur à la pathlib
, pour gérer les chemins de fichiers avec des fonctions comme exists
, dirname
, basename
ou encore splitext
.
Le module propose aussi une fonction chdir
(pour change directory) qui prend un chemin (relatif ou absolu) en argument et permet de changer le répertoire courant.
>>> Path.cwd()
PosixPath('/home/antoine')
>>> os.chdir('..')
>>> Path.cwd()
PosixPath('/home')
Attention, changer de répertoire courant affecte ensuite toutes les opérations utilisant des chemins relatifs, c’est une opération à réaliser avec précaution.
Parmi les autres outils présents dans le module, on trouve par exemple la fonction cpu_count
qui permet de savoir combien de cœurs sont disponibles sur la machine.
>>> os.cpu_count()
8
Gestion de l’environnement
Un programme est toujours exécuté dans un certain environnement.
Cet environnement consiste en un ensemble de variables définies par le système, sur lesquelles les programmes peuvent se baser pour certaines de leurs actions.
Il est ainsi courant de trouver des variables d’environnement telles que SHELL
(le shell utilisé), USER
(l’utilisateur courant), LANG
(la langue de l’utilisateur), HOME
(le dossier de l’utilisateur) ou PWD
(le répertoire courant).
Depuis le shell, on peut spécifier des variables d’environnement supplémentaires pour un programme en plaçant VAR=value
avant l’invocation du programme.
% OUTPUT=/tmp/out MAX_VALUE=256 python script.py
Il est coutume d’utiliser exclusivement des lettres capitales (ainsi que des chiffres et des underscores) dans les noms de variables d’environnement.
En Python, l’environnement est accessible via le dictionnaire environ
du module os
.
Ce dictionnaire associe les valeurs des variables d’environnement à leurs noms.
>>> import os
>>> os.environ
environ({..., 'OUTPUT': '/tmp/out', 'MAX_VALUE': '256'})
On le voit, les valeurs des variables d’environnement sont toujours des chaînes de caractères, il peut alors être nécessaire de les convertir.
>>> os.environ['MAX_VALUE']
'256'
>>> int(os.environ['MAX_VALUE'])
256
Le module dispose aussi d’une fonction getenv
pour récupérer une variable d’environnement.
>>> os.getenv('OUTPUT')
'/tmp/out'
La fonction renvoie None
si la variable d’environnement n’est pas définie, mais il est possible de lui spécifier un argument default
pour choisir cette valeur par défaut.
>>> os.getenv('NOTFOUND')
>>> os.getenv('NOTFOUND', 'no')
'no'
Le dictionnaire environ
est bien sûr éditable, ce qui permet de faire évoluer l’environnement du programme.
>>> os.environ['MAX_VALUE'] = str(int(os.environ['MAX_VALUE']) * 2)
>>> os.getenv('MAX_VALUE')
'512'
Afin de traiter l’environnement comme des chaînes d’octets, on trouve aussi le dictionnaire environb
et la fonction getenvb
qui remplissent le même rôle que environ
et getenv
.
>>> os.environb
environ({..., b'OUTPUT': b'/tmp/out', b'MAX_VALUE': b'512'})
>>> os.getenvb(b'MAX_VALUE')
b'512'
Pour plus d’informations, vous pouvez consulter la documentation du module os
.