Manipulation de fichiers

Dans les tutoriels précédents, nous avons utilisé systématiquement des variables pour stocker des données et réaliser des opérations sur celles-ci. Cette façon de faire peut suffire dans le cadre d’une session Python donnée, comme ici dans un notebook Jupyter ou bien dans un programme.

Mais que se passe-t-il par exemple si l’on souhaite conserver les sorties des calculs réalisés ou bien des données transformées une fois la session terminée ? Il nous faut alors sauvegarder ces éléments dans un fichier, à un endroit où ces données vont persister dans le temps en vue d’une utilisation ultérieure. Dans ce tutoriel, nous allons voir comment lire et écrire des fichiers avec Python.

Quelques notions sur les modules et les packages

Avant de parler de manipulation de fichiers, nous devons faire un bref détour par le monde des modules et des packages (librairies).

Jusqu’à maintenant, nous avons essentiellement utilisé des objets et des instructions standards de Python, qui ne nécessitaient donc pas d’import tiers. Dans ce tutoriel et tous ceux qui vont suivre, nous allons réaliser des opérations plus complexes (interagir avec un système de fichiers, faire du calcul vectoriel, manipuler des données tabulaires, etc.) qu’il serait très coûteux, inefficient, et avec un potentiel d’erreur énorme, de coder à la main en utilisant les objets de base de Python.

C’est pourquoi nous allons utiliser des packages, sortes de boîtes à outils remplies de fonctions et de classes développées par d’autres (souvent, de manière communautaire) et qui permettent de réaliser des opérations complexes à moindre coût.

Terminologie

Commençons par quelques brefs éléments de terminologie pour bien se repérer dans l’écosystème Python :

  • un module est un fichier texte (portant l’extension .py pour bien marquer le lien à Python) contenant un ensemble de définitions (de classes, de fonctions) et d’instructions, que l’on peut importer dans un environnement Python afin de les utiliser.

  • un package est un ensemble de modules réunis dans un même répertoire

Par exemple, nous allons voir en détails dans la prochaine partie l’utilisation de numpy. numpy est un package qui permet de faire du calcul scientifique sur des objets multidimensionnels. Pour ce faire, numpy met à disposition un nombre gigantesque de fonctions et d’outils. Toutes les mettre dans un seul et même module serait franchement illisible. Ainsi, numpy est structuré en différents modules qui groupent les fonctions réalisant des opérations similaires : les fonctions générant de l’aléatoire dans le module random, celles réalisant de l’algèbre linéaire dans le module linalg, etc.

Import de module

Pour pouvoir exploiter les fonctions d’un module et les différents modules qui constituent un package, il nous faut en premier lieu les importer.

La syntaxe est très simple, illustrons là à travers un exemple.

import random
random.randint(0, 100)
88

Nous avons importé le module random (complet) de la librairie standard de Python via l’instruction import. Ensuite, nous avons fait appel à la fonction randint contenue dans le module random, qui renvoie un nombre aléatoire entre a et b ses paramètres.

On aurait pu également importer seulement la fonction randint en utilisant la syntaxe from module import fonction. Il n’est alors plus nécessaire de spécifier le nom du module lorsqu’on appelle la fonction.

from random import randint
randint(0, 100)
43

Notons qu’une fois qu’un import est effectué, le module importé est disponible pour toute la durée de la session Python. Il n’y a donc pas besoin d’importer le module avant chaque utilisation d’une de ses fonctions, une fois au début de son notebook ou script suffit.

Trop d’imports tue l’import

Il arrive parfois de voir la syntaxe from module import * (* s’appelle le wildcard) qui a pour effet d’importer en mémoire toutes les fonctions du module. Si cela permet de gagner du temps, ce n’est pourtant pas une bonne pratique :

  • d’une part, cela charge plus de code en mémoire qu’il n’est nécessaire pour notre application ;

  • d’autre part, cela limite la lisibilité du code dans la mesure où l’on ne voit pas explicitement d’où ont été importées les fonctions qui sont utilisées dans le code.

Import de package

Un package est simplement une collection de modules, structurée selon une arborescence. La syntaxe pour importer un package est identique à celle pour importer un module.

Par exemple, regardons à nouveau comment utiliser la fonction randint, mais cette fois celle du package numpy (qui fait la même chose).

import numpy
numpy.random.randint(0, 100)
87

On a importé le package numpy, qui nous a permis d’accéder via son module random à la fonction randint. Là encore, on aurait pu importer directement la fonction.

from numpy.random import randint
randint(0, 100)
79

En pratique, la première syntaxe est préférable : il est toujours plus lisible de montrer explicitement d’où vient la fonction que l’on appelle. Pour réduire la verbosité, il est fréquent de donner un alias aux packages que l’on importe. Voici les trois plus fréquents, que l’on rencontrera très souvent dans les tutoriels du prochain chapitre sur la manipulation de données.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

On peut alors utiliser ces alias pour appeler des modules et des fonctions.

x = np.linspace(0, 10, 1000)
plt.plot(x, np.sin(x))

Installation de packages

Jusqu’à maintenant, nous avons pu importer sans problème les différents packages via l’instruction import. Mais comment ont-ils été installés ? Il faut distinguer deux cas de figure :

  • un certain nombre de packages font partie de la bibilothèque standard, ce qui signifie qu’ils sont installés en même temps que Python. C’est par exemple le cas du package random utilisé plus haut, mais il en existe beaucoup d’autres ;

  • les autres packages “tiers” sont développés par la communauté des utilisateurs de Python, et doivent être installés pour pouvoir être utilisés. C’est notamment le cas de numpy et pandas. Dans notre cas, nous n’avons pas eu à les installer car l’environnement fourni pour la formation contient déjà l’ensemble des packages nécessaires pour exécuter les différents chapitres.

Illustrons l’installation de package à travers le package emoji, qui permet de représenter des émoticônes dans les sorties de Python. Pour le coup, celui-ci n’est pas encore installé ; essayer de l’importer produit une ModuleNotFoundError.

import emoji
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[9], line 1
----> 1 import emoji

ModuleNotFoundError: No module named 'emoji'

Pour installer un package, la commande est simple : pip install nom_du_package. Sans rentrer dans les détails, pip est un gestionnaire de packages, installé avec Python, qui s’utilise en ligne de commande (i.e. dans un terminal). Pour pouvoir envoyer une commande au terminal depuis un notebook Jupyter, on rajoute un ! devant la commande.

!pip install emoji
Collecting emoji
  Downloading emoji-2.14.0-py3-none-any.whl.metadata (5.7 kB)
Downloading emoji-2.14.0-py3-none-any.whl (586 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 0.0/586.9 kB ? eta -:--:--   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 586.9/586.9 kB 45.5 MB/s eta 0:00:00
Installing collected packages: emoji
Successfully installed emoji-2.14.0

Il est à présent possible d’importer le package et d’utiliser ses fonctions.

import emoji

print(emoji.emojize('Python est :thumbs_up:'))
Python est 👍
pip et PyPI

pip est le gestionnaire de paquets standard pour Python. Il permet d’installer, de mettre à jour et de supprimer des packages Python trouvés dans le Python Package Index (PyPI), un répertoire de packages pour la programmation en Python. Ce répertoire contient un nombre gigantesque de projets (environ 500,000 à l’heure de l’écriture de ce tutoriel), des plus amateurs aux plus essentiels.

De manière générale, il est toujours préférable, avant de se lancer dans l’écriture d’une application “à la main”, de vérifier qu’un package faisant la même chose ou presque n’existe pas déjà. Une simple recherche google - de préférence en Anglais - contenant les mots-clés de ce que l’on cherche à faire permet souvent de s’en assurer.

Manipulation de fichiers

Interagir avec le système de fichiers local

Pour pouvoir lire et écrire des fichiers avec Python, il nous faut d’abord comprendre comment ceux-ci sont représentés sur le système de fichiers (file system) local, et comment Python interagit avec ce dernier.

Le module pathlib

Pour ce faire, nous allons utiliser de manière répétée le module pathlib et en particulier la classe Path. Ce module permet d’interagir avec le système de fichiers sous forme d’objets, en manipulant des attributs et leurs méthodes. Pas de panique, nous avons vu tout ce qu’il nous fallait savoir à ce propos dans le précédent tutoriel.

from pathlib import Path

Propriétés d’un fichier

Un fichier a deux propriétés :

  • un nom de fichier

  • un chemin (path), qui spécifie sa localisation dans le système de fichiers.

A titre d’exemple, regardons les fichiers qui se trouvent dans notre répertoire courant (par défaut, le dossier dans lequel se trouve ce notebook). La méthode à utiliser s’appelle cwd, pour current working directory.

Path.cwd()
PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files')

Le chemin de notre répertoire courant est contenu dans un objet PosixPath, ce qui signifie simplement que pathlib a compris que nous étions sur un environnement de type Unix (les serveurs du SSP Cloud sont sous Linux). Si vous exécutiez ce notebook en local sur un ordinateur Windows, l’objet serait WindowsPath. Concrètement, cela ne change pas grand chose pour vous mais c’est en fait assez important : les systèmes de fichiers n’utilisent pas les mêmes conventions entre les différents environnements (ex : les séparateurs entre dossiers dans un chemin ne sont pas les mêmes), mais pathlib vous permet d’interagir avec ces différents systèmes de manière harmonisée.

Maintenant, listons tous les fichiers contenus dans notre répertoire courant. On utilise pour cela une seconde méthode glob qui va simplement renvoyer tous les fichiers dont le nom a une certaine structure. Par exemple, .glob('*.txt') va récupérer tous les fichiers dont l’extension est .txt et .glob('test.*') va récupérer tous les fichiers dont le nom est test, quelle que soit leur extension. Ici, on récupère tous les fichiers en utilisant des wildcards * aux deux positions.

Cette méthode renvoie un objet un peu spécial (un générateur). Si vous vous rappelez bien, on avait déjà rencontré le même cas avec la fonction range. Il suffit d’appeler la fonction list sur le tout pour afficher les résultats de manière lisible.

Path.cwd().glob('*.*')
<generator object Path.glob at 0x7f3a1e718c80>
list(Path.cwd().glob('*.*'))
[PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/tutorial.qmd'),
 PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/tutorial.quarto_ipynb'),
 PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/notes_clean.txt'),
 PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/notes.txt'),
 PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/write_list.py'),
 PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/normalisation.py'),
 PosixPath('/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/gamme.txt')]

On retrouve notre notebook, un fichier qui contient les solutions des exercices du tutoriel, et un certain nombre de fichiers texte qui vont servir d’exemples dans la suite du tutoriel. Si l’on prend le notebook par exemple, on distingue bien :

  • son nom de fichier : tutorial.ipynb

  • son chemin : /home/onyxia/work/

Chemins absolus et chemins relatifs

Il y a deux manières possibles de spécifier le chemin d’un fichier :

  • de manière absolue, le chemin commence alors par la racine (/ en Unix, C:\ en Windows, etc.). Les chemins renvoyés ci-dessus sont donc absolus.

  • de manière relative, i.e. relativement au répertoire courant du programme Python. Dès lors qu’un chemin ne commence pas par la racine, pathlib va le considérer relatif.

Cette distinction va s’avérer assez importante par la suite, lorsqu’il sera question de lire et d’écrire des fichiers.

Former des chemins

En pratique, ce qui nous intéresse est de pouvoir constituer nos propres chemins – qu’ils soient absolus ou relatifs au répertoire courant – afin de spécifier où se trouvent les fichiers que nous souhaitons lire ou bien où doivent se trouver les fichiers que l’on souhaite écrire.

pathlib offre une syntaxe très intuitive pour constituer des chemins, très similaire à la concaténation des chaînes de caractères que nous avons déjà vue. Au lieu d’un +, on va cette fois utiliser un / pour concaténer les différentes parties d’un chemin.

Par exemple, essayons de reconstruire le chemin complet de ce notebook. On peut commencer par trouver le chemin du home directory, qui est le dossier standard dans lequel se trouvent tous les fichiers de l’utilisateur.

Path.home()
PosixPath('/home/runner')

On peut alors concaténer les différents sous-dossier et le nom de fichier du notebook pour obtenir le chemin complet vers celui-ci.

path_nb = Path.home() / 'work' / 'tutorial.ipynb'
path_nb
PosixPath('/home/runner/work/tutorial.ipynb')

On retrouve bien exactement le même chemin que celui obtenu en listant les fichiers présents dans le répertoire courant.

Plus sur pathlib

Nous n’avons vu qu’un aperçu des outils qu’offre le module pathlib pour interagir avec le système de fichiers local. La documentation officielle présente de manière exhaustive ces possibilités. Nous présenterons dans ce tutoriel et dans les suivants d’autres méthodes issues de cette librairie, à mesure que l’occasion se présente. Pour l’heure, nous en savons suffisamment pour lire et écrire des fichiers sur le système de fichiers.

Fichiers texte et fichiers binaires

En programmation, on est généralement amenés à manipuler deux grandes familles de fichiers bien différentes :

  • les fichiers texte. Ils ne contiennent que des caractères textuels standards – techniquement, qui respectent le standard Unicode – sans informations de formatting (police, couleur, etc.). Les fichiers .txt ou encore les scripts Python finissant en .py sont des exemples de fichiers texte. Ces fichiers peuvent être lus avec n’importe quel éditeur de texte.

  • les fichiers binaires. Ce sont en fait tous les autres types de fichiers : fichiers compressés (.zip, tar.gz, etc.), documents PDFs, images, programmes, etc. Ouvrir un tel fichier avec un éditeur de texte produit généralement une grande suite de caractères incompréhensibles, car la représentation textuelle n’est pas adaptée à ces données.

Comme vous pouvez l’imaginer, ces deux types de fichier se traitent avec des outils différents. Par ailleurs, du fait de la diversité des fichiers binaires, chacun de ses fichiers nécessite un traitement particulier. Dans un contexte de programmation, on est cependant principalement à manipuler du code, qui est une donnée textuelle. On va donc s’intéresser uniquement à l’écriture et à la lecture de fichiers texte dans ce tutoriel, mais il est important de savoir reconnaître des données binaires lorsqu’on est amené à en traiter.

Ouvrir un fichier

Demander à Python d’ouvrir un fichier revient à ouvrir une connexion entre l’environnement Python sur lequel vous êtes et le fichier. Tant que cette connexion est ouverte, il est possible de manipuler le fichier.

Pour ouvrir un fichier, on utilise la fonction open. On va par exemple ouvrir le fichier gamme.txt qui a été mis dans le répertoire courant.

path_gamme = Path.cwd() / 'gamme.txt'
file_in = open(path_gamme, 'r')
file_in
<_io.TextIOWrapper name='/home/runner/work/formation-python-initiation/formation-python-initiation/source/manipulation/modules-files/gamme.txt' mode='r' encoding='UTF-8'>

La fonction open renvoie un objet de type _io.TextIOWrapper, qui spécifie le mode d’encodage du fichier et le mode d’ouverture.

L’encodage et le décodage sont des sujets techniques, que nous n’aborderons pas dans ce tutoriel. Retenons simplement que le mode d’encodage par défaut est l’UTF-8, et qu’il n’y a jamais vraiment de bonne raison de choisir un autre mode.

En revanche, le mode d’ouverture est très important. Il y a trois modes principaux :

  • r : lecture seule. Le fichier ne peut qu’être lu, mais pas modifié. C’est le mode par défaut lorsqu’on ne spécifie aucun mode.

  • w : écriture. Il est possible dans ce mode d’écrire sur un fichier. Attention : si un fichier avec le même nom existe déjà, il sera automatiquement écrasé.

  • a : appending. Ce mode ne permet que de rajouter des lignes à la fin d’un fichier existant.

Une fois le fichier ouvert, on peut réaliser des opérations sur ce fichier à l’aide de méthodes attachées à l’objet qui le représente. On verra dans la section suivante ce que fait la méthode readlines.

file_in.readlines()
['do\n', 're\n', 'mi\n', 'fa\n', 'sol\n', 'la\n', 'si']

Une fois les manipulations terminées, on ferme la connexion avec la méthode close. Il n’est alors plus possible de manipuler le fichier.

file_in.close()

En pratique, on oublie facilement de fermer la connexion à un fichier, ce qui peut créer des erreurs pénibles. Il existe une syntaxe qui permet d’éviter ce problème en utilisant un context manager qui gère toute la connexion pour nous.

with open(path_gamme, 'r') as file_in:
    lines = file_in.readlines()
    
lines
['do\n', 're\n', 'mi\n', 'fa\n', 'sol\n', 'la\n', 'si']

Cette syntaxe est beaucoup plus lisible : grâce à l’indentation, on voit clairement les opérations qui sont effectuées tant que le fichier est ouvert, et ce dernier est automatiquement fermé dès lors que l’on revient au niveau initial d’indentation. On préférera toujours utiliser cette syntaxe si possible, c’est une bonne pratique de programmation.

Lire un fichier

Une fois un fichier ouvert, on peut vouloir lire son contenu. Il existe différentes manières de faire. Une méthode simple et élégante est de parcourir le fichier à l’aide d’une boucle, ce qui est rendu possible par le fait que l’objet Python représentant le fichier est itérable.

with open(path_gamme, 'r') as file_in:
    for line in file_in:
        print(line)
do

re

mi

fa

sol

la

si

Dans notre exemple, nous avons simplement affiché les lignes, mais on peut faire de nombreuses choses à partir des données présentes dans le fichier texte : les stocker dans un objet Python, les utiliser pour faire des calculs, ne conserver que les lignes qui répondent à une condition donnée via une instruction if, etc.

Il existe également des méthodes toutes faites pour lire le contenu d’un fichier. La plus basique est la méthode read, qui retourne l’ensemble du fichier comme une (potentiellement très longue) chaîne de caractères.

with open(path_gamme, 'r') as file_in:
    txt = file_in.read()
    
txt
'do\nre\nmi\nfa\nsol\nla\nsi'

C’est rarement très utile : on préfère en général récupérer individuellement les lignes d’un fichier. La méthode readlines parcourt le fichier complet, et renvoie une liste dont les éléments sont les lignes du fichier, dans l’ordre d’apparition.

with open(path_gamme, 'r') as file_in:
    l = file_in.readlines()
    
l
['do\n', 're\n', 'mi\n', 'fa\n', 'sol\n', 'la\n', 'si']

Notons que chaque élément de la liste (sauf le dernier) se termine par le caractère spécial \n (“retour à la ligne”) qui marque simplement la fin de chaque ligne dans un fichier texte. C’est la présence (cachée) de ce même caractère à la fin de chaque appel à la fonction print qui fait que l’on revient à la ligne à chaque fois que l’on utilise un print.

Écrire dans un fichier

L’écriture dans un fichier est très simple, elle s’effectue à l’aide de la méthode write. Par exemple, écrivons dans un fichier ligne à ligne les différents éléments contenus dans une liste.

ex = ["ceci", "est", "un", "exemple", "très", "original"]

with open("test.txt", "w") as file_out:
    for elem in ex:
        file_out.write(elem)

Tout semble s’être passé sans encombre. On peut vérifier que notre fichier a bien été crée via l’explorateur de fichier de Jupyter (sur la gauche) ou bien via la commande ls dans le terminal.

!ls
gamme.txt     notes.txt    test.txt  tutorial.quarto_ipynb
normalisation.py  notes_clean.txt  tutorial.qmd  write_list.py

Il est bien là. Vérifions maintenant que son contenu est bien celui que l’on souhaitait.

with open("test.txt", "r") as file_out:
    print(file_out.read())
ceciestunexempletrèsoriginal

Les différents éléments de notre liste se sont fusionnés en un seul bloc de texte ! C’est parce que, contrairement à la fonction print par exemple, la fonction write n’ajoute pas automatiquement le caractère de retour à la ligne. Il faut l’ajouter manuellement.

with open("test.txt", "w") as file_out:
    for elem in ex:
        file_out.write(elem + "\n")
        
with open("test.txt", "r") as file_out:
    print(file_out.read())
ceci
est
un
exemple
très
original

C’est beaucoup mieux.

Quelques remarques supplémentaires sur l’écriture de fichiers :

  • mieux vaut le répéter : utiliser le mode d’ouverture \w pour un fichier écrase complètement son contenu. Lorsqu’on a réécrit notre fichier avec les sauts de ligne, on a complètement écrasé l’ancien.

  • pourquoi a-t-on pu mettre juste le nom du fichier dans la fonction open et pas un objet Path comprenant le chemin complet vers le fichier que l’on souhaitait créer ? C’est parce que Python l’a automatiquement interprété comme un chemin relatif (à notre répertoire courant) du fait de l’absence de racine.

  • on ne peut écrire dans un fichier que des éléments de type str (chaîne de caractère). Si un des éléments de la liste ci-dessus avait été de type int ou float par exemple, il aurait fallu le convertir via la fonction str() avant de l’écrire dans le fichier. Sinon, Python aurait renvoyé une erreur.

Exécuter du code depuis des fichiers .py

Jusqu’à présent dans ce tutoriel, nous avons exploré l’utilisation de packages/modules, qu’ils proviennent de la bibliothèque standard de Python ou soient développés par des tiers. Nous avons également abordé l’interaction avec le système de fichiers local. À présent, découvrons comment combiner ces compétences en écrivant et exécutant nos propres scripts et modules Python sous forme de fichiers .py.

Les scripts Python

Dans un environnement de notebook Jupyter (comme celui dans lequel vous vous trouvez), le code Python est exécuté de manière interactive, cellule par cellule. Cela est possible car un kernel (noyau) Python tourne en arrière-plan pendant tout le long de la session d’utilisation du notebook. Cependant, en dehors de Jupyter, le code est généralement écrit et exécuté sous forme de scripts. Un script Python est simplement un fichier texte portant l’extension .py et qui contient une série d’instructions Python qui vont être exécutées linéairement par l’interpréteur Python.

Le fichier write_list.py reprend une cellule de code vue précédemment. Affichons son contenu.

with open('write_list.py', 'r') as script:
    print(script.read())
ex = ["ceci", "est", "un", "exemple", "très", "original"]

with open("output_script.txt", "w") as file_out:
    for elem in ex:
        file_out.write(elem)

print("Succès !")

Un script Python s’exécute dans un terminal via la commande python nom_du_script.py. Pour l’exécuter depuis un notebook Jupyter, on rajoute là encore un ! en début de ligne.

!python write_list.py
Succès !

Le fichier output_script.txt a bien été créé en local (il faut parfois attendre un peu ou actualiser pour qu’il s’affiche) et le message attendu a été imprimé dans la sortie de la console.

Notebook vs. scripts

Faut-il préférer l’utilisation de notebooks Jupyter, comme dans le cadre de cette formation, ou bien préférer l’exécution via des scripts ? Il n’y a pas de réponse définitive à cette question :

  • les notebooks permettent une exécution interactive, très pratique pour l’expérimentation ;

  • les scripts rendent plus facile l’automatisation d’une procédure, dans la mesure où ils sont exécutés linéairement et sans requérir d’actions intermédiaires de la part de l’utilisateur.

En somme, les notebooks sont très utiles durant la phase de développement, mais on préfèrera les scripts dès lors qu’il est question d’automatiser des traitements ou de produire du code visant à tourner en production.

Scripts et modules

Comme nous l’avons vu, un script est un fichier .py destiné à être exécuté directement. Il contient généralement un flux de travail complet ou une tâche automatisée. Un module est également un fichier .py, mais qui contient des définitions de fonctions et/ou de classes destinées à être utilisées par d’autres scripts ou modules. Il n’est pas destiné à être exécuté seul mais importé ailleurs. Au début de ce tutoriel, nous avons utilisé des modules issus de packages écrits par d’autres. Voyons maintenant comment l’on peut écrire nos propres modules et les importer selon les mêmes principes.

Affichons le contenu du fichier normalisation.py qui nous servira d’exemple.

with open('normalisation.py', 'r') as module:
    print(module.read())
import numpy as np


def normalise(x):
    """Normalise un vecteur de valeurs à une moyenne de 0 et un écart-type de 1."""
    return (x - np.mean(x)) / np.std(x)


if __name__ == "__main__":
    vec = [2, 4, 6, 8, 10]
    vec_norm = normalise(vec)
    print(np.mean(vec), np.var(vec), np.mean(vec_norm), np.var(vec_norm))

La fonction contenue dans ce module peut être importée comme nous l’avons vu dans ce tutoriel. Notons que le module doit lui même importer les packages/modules nécessaires au bon fonctionnement des fonctions qu’il contient (en l’occurence, numpy).

Pour importer un module local, on utilise l’instruction import suivie du nom du fichier, sans l’extension. Toutes les fonctions définies dans le module peuvent alors être utilisées via la syntaxe nom_du_module.nom_de_la_fonction.

import normalisation

x = [1, 2, 3, 4, 5]
x_norm = normalisation.normalise(x)

print(x_norm)
[-1.41421356 -0.70710678  0.          0.70710678  1.41421356]

Comme expliqué en début de chapitre, on pourrait également importer la fonction directement afin de ne pas avoir à rappeler le nom du module qui la contient. C’est notamment pratique si cette fonction est amenée à être utilisée plusieurs fois dans un même notebook/script.

from normalisation import normalise

x = [1, 2, 3, 4, 5]
x_norm = normalise(x)

print(x_norm)
[-1.41421356 -0.70710678  0.          0.70710678  1.41421356]
La fausse bonne idée : import *

Une bonne pratique essentielle est de favoriser la lisibilité de son code. Dans les deux variantes d’import présentées ci-dessus, le code est lisible : on voit bien de quel module provient la fonction utilisée.

En revance, il n’est pas rare de voir dans du code Python l’instruction from mon_module import *, qui permet d’importer toutes les fonctions définies dans le fichier mon_module.py. C’est à proscrire dans la mesure du possible pour deux raisons :

  • il devient difficile de déterminer de quel module ou package proviennent les fonctions utilisées ;
  • si des fonctions importées à partir de différents packages/modules ont le même nom, elles peuvent se remplacer et générer des erreurs pénibles à débugger.

Afin de limiter la longueur de la ligne d’instruction en cas d’import de multiples fonctions, on peut adopter la syntaxe suivante :

from mon_module import (
    fonction1,
    fonction2,
    fonction3
)
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[34], line 1
----> 1 from mon_module import (
      2     fonction1,
      3     fonction2,
      4     fonction3
      5 )

ModuleNotFoundError: No module named 'mon_module'

Enfin, notons qu’un fichier .py peut à la fois servir comme module et comme script. Pour différencier les deux usages, on utilise la variable __name__, qui est définie par défaut par Python dès lors que l’on utilise un fichier .py :

  • si le fichier est utilisé comme un module (ex : import mon_fichier), la variable __name__ vaut le nom du fichier (ex : mon_fichier)

  • si le fichier est utilisé comme un script (ex : python mon_fichier.py), la variable __name__ vaut __main__

Dans la cellule précédente, le fichier normalisation.py était importé comme un module. Dans ce cas, la variable __name__ vaut normalisation et c’est pourquoi le code sous la condition if n’a pas été exécuté. Lorsque le fichier est exécuté comme script, ce code est exécuté.

!python normalisation.py
6.0 8.0 0.0 0.9999999999999998

Il est dont très fréquent de croiser la condition if __name__ == "__main__" dans les scripts Python, qui distingue l’usage comme module et l’usage comme script.

Exercices

Questions de compréhension

  • 1/ Qu’est-ce qu’un module ?

  • 2/ Qu’est ce qu’un package ?

  • 3/ Pourquoi n’est-ce pas une bonne pratique d’importer toutes les fonctions d’un module avec la syntaxe from module import *

  • 4/ Quels sont les avantages de la librairie pathlib ?

  • 5/ Quelles sont les deux propriétés d’un fichier qui permettent de repérer sa position dans le système de fichiers ?

  • 6/ Qu’est-ce que le répertoire courant ?

  • 7/ Quelles sont les deux manières de spécifier un chemin ? Comment Python fait-il la différence entre les deux ?

  • 8/ Quels sont les deux grandes familles de fichiers que l’on est amenés à manipuler en programmation ?

  • 9/ Quels sont les différents modes d’ouverture d’un fichier ?

  • 10/ Pourquoi est-il préférable d’utiliser la syntaxe with open(...) as ... pour ouvrir un fichier ?

  • 11/ Pourquoi peut-on parcourir les lignes d’un fichier à l’aide d’une boucle ?

  • 12/ Quelle est la différence entre un module et un script ?

Afficher la solution
  • 1/ un module est un fichier texte (portant l’extension .py pour bien marquer le lien à Python) contenant un ensemble de définitions (de classes, de fonctions) et d’instructions

  • 2/ Un package est une collection de modules.

  • 3/ Cela surcharge la mémoire si l’on a besoin que de quelques fonctions, et cela réduit la lisibilité puisqu’on ne sait pas directement de quel module provient quelle fonction.

  • 4/ Elle permet d’interagir avec le système de fichiers avec une syntaxe de POO unifiée, quel que soit l’environnement sur lequel on travaille.

  • 5/ Nom de fichier et chemin du dossier qui contient le fichier.

  • 6/ C’est le répertoire dans lequel la session Python courante est ouverte. Dans le cadre d’un notebook Jupyter, c’est par défaut le dossier qui le contient.

  • 7/ Chemin absolu (complet) et chemin relatif (relatif au répertoire courant). Un chemin absolu se reconnaît car il part toujours de la racine du système de fichiers.

  • 8/ Fichiers textes et fichiers binaires (tout ce qui n’est pas du texte).

  • 9/ r : lecture. w : écriture. a : appending

  • 10/ Cette syntaxe fait intervenir un context manager, qui gère la connexion au fichier (ouverture et fermeture) pour nous.

  • 11/ Car l’objet représentant le fichier en Python est un itérable.

  • 12/ Comme nous l’avons vu, un script est un fichier .py destiné à être exécuté directement. Il contient généralement un flux de travail complet ou une tâche automatisée. Un module est également un fichier .py, mais qui contient des définitions de fonctions et/ou de classes destinées à être utilisées par d’autres scripts ou modules. Il n’est pas destiné à être exécuté seul mais importé ailleurs.

Moyenne et écart-type des notes obtenues à un examen

Exercice inspiré de : python.sdv.univ-paris-diderot.fr

Le fichier texte notes.txt se trouve dans votre répertoire courant. Il contient les notes obtenues par 50 élèves à un examen. Problème : toutes les notes ont été écrites sur une même ligne, avec un espace à chaque fois. Ouvrez ce fichier et calculez la moyenne et l’écart-type des notes.

Indices :

  • les chaînes de caractère ont une méthode split qui permet de séparer du texte selon un caractère donné

  • il faudra convertir les notes au format numérique pour pouvoir leur appliquer des fonctions mathématiques

  • vous pouvez utiliser les fonctions du package numpy pour calculer les statistiques demandées

# Testez votre réponse dans cette cellule
Afficher la solution
import numpy as np

with open("notes.txt", "r") as file_in:
    notes = file_in.read()

notes = notes.split()
notes_num = []
for n in notes:
    notes_num.append(int(n))

print(np.mean(notes_num))
print(np.std(notes_num))

Admis ou recalé

Exercice inspiré de : python.sdv.univ-paris-diderot.fr

Le fichier texte notes_clean.txt se trouve dans votre répertoire courant. Il contient les notes obtenues par 50 élèves à un examen. Contrairement à l’exercice précédent, les notes sont cette fois bien écrites : une note par ligne.

Écrire un code qui :

  • stocke chaque note au format int dans une liste

  • réécrit les notes dans un fichier notes_mentions.txt avec sur chaque ligne la note, suivie d’un espace, suivi de la mention “admis” si la note est supérieure ou égale à 10, et “recalé” sinon.

Par exemple, les trois premières lignes de ce nouveau fichier devraient être :

5 recalé
5 recalé
18 admis
# Testez votre réponse dans cette cellule
Afficher la solution
notes = []

with open("notes_clean.txt", "r") as file_in:
    for n in file_in:
        notes.append(int(n))
        
with open("notes_mentions.txt", "w") as file_out:
    for n in notes:
        if n >= 10:
            mention = "admis"
        else:
            mention = "recalé"
        file_out.write(str(n) + " " + mention + "\n")

Retardataires

3 élèves n’avaient pas rendu leur copie dans les temps pour l’examen :

  • Miranda a obtenu 16 et a rendu son devoir avec 3 jours de retard

  • Paolo a obtenu 11 et a rendu son devoir avec 1 jour de retard

  • Isidore a obtenu 3 et a rendu son devoir avec 5 jours de retard.

Chaque élève aura une note finale égale à la note obtenue moins le nombre de jours de retard. Une note ne pouvant être négative, elle sera remplacée par 0.

Les informations nécessaires ont été placées dans une liste dans la cellule suivante. A l’aide d’une boucle sur cette liste, ajouter (sans réécrire complètement le fichier !). les notes au fichier notes_clean.txt (sans la mention).

NB : si vous avez écrasé le contenu d’un fichier par erreur, vous pouvez retrouver les fichiers propres sur le dépôt GitHub associé à la formation.

supp = [(16, 3), (11, 1), (3, 5)]
# Testez votre réponse dans cette cellule
Afficher la solution
supp = [(16, 3), (11, 1), (3, 5)]

with open("notes_clean.txt", "a") as file_out:
    for elem in supp:
        note_finale = elem[0] - elem[1]
        note_finale = max(0, note_finale)
        file_out.write(str(note_finale) + "\n")

Scanner des fichiers

Écrire un programme qui réalise les opérations suivantes :

  • dans le répertoire courant, lister les chemins des fichiers dont l’extension est .txt (la syntaxe a été vue dans la partie sur pathlib)

  • faire une boucle qui parcourt ces chemins et ouvre chaque fichier séquentiellement

  • pour chaque fichier, faire un test d’appartenance (rappel de la syntaxe : if pattern in string: ...) pour tester si le fichier contient le mot “sol”. Si c’est le cas, imprimer son chemin absolu dans la console (seul le chemin du fichier gamme.txt devrait donc apparaître)

# Testez votre réponse dans cette cellule
Afficher la solution
from pathlib import Path

txt_files_paths = list(Path.cwd().glob('*.txt'))

for path in txt_files_paths:
    with open(path, "r") as file_in:
        content = file_in.read()
        if "sol" in content:
            print(path)