Tour d'horizon de la bibliothèque standard

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 et floordiv) 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 | deviennent and_ et or_, suffixés d’un _ pour ne pas générer de conflit avec les mots-clés and et or. De même que not devient not_.

    >>> 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 fonction ixxx pour l’opérateur en-place (par-exemple iadd pour +=).

    >>> values = []
    >>> operator.iadd(values, [42])
    [42]
    >>> values
    [42]
    
  • Les opérateurs foo[key], foo[key] = value et del foo[key] sont appelés getitem, setitem et delitem. getitem renvoie la valeur demandée, setitem et delitem renvoient None.

    >>> 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 13\frac{1}{3} 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 13\frac{1}{3} (1/3), 1510\frac{15}{10} (1.5) ou encore 82\frac{8}{2} (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 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.