Un programme doit pouvoir interagir avec le système d’exploitation : fichiers, (et bases de données), périphériques, processus, réseau… Ce cours introduit la programmation système :
- arguments d’un programme,
- système de fichiers,
- lecture et l’écriture de fichiers textes et binaires.
Comme pour les fonctions, les arguments d’un programme sont ses paramètres
d’entrée pour adapter son comportement sans modifier son code. En Python,
ils sont accessibles dans la variable sys.argv (module sys à importer)
qui est de type List (tableau).
Les arguments peuvent notamment être des chemins de fichiers.
Exemple
import sys
if __name__ == "__main__":
print("'sys.argv' est un tableau : " + str(sys.argv), end="\n\n")
print("sys.argv[0] = " + sys.argv[0] + " - c'est toujours le programme")
for i in range(1, len(sys.argv)):
print("sys.argv[" + str(i) + "] = " + sys.argv[i])
Exécuter ce programme : python3 script.py essai "autre chose" …
Erreurs et codes de sortie
Habituellement, lorsqu’un programme attend des arguments (paramètres), une des premières choses à faire est de vérifier le nombre d’arguments attendus :
import sys
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage : " + sys.argv[0] + " argument_attendu", file=sys.stderr)
sys.exit(1) #toute valeur de sortie ≠ 0 indique une erreur
C’est une bonne pratique d’émettre le message d’erreur vers stderr (standard
error — 2 dans le shell) plutôt que vers la sortie par défaut stdout (standard
output). Ils peuvent ainsi être masqués ou redirigés vers un journal d’erreur ; exemple :
python3 script.py 2> /dev/null
python3 script.py 2> error.log
cat error.log
Si les arguments sont obligatoires, il est recommandé — pour faciliter son intégration avec d’autres outils — de quitter le programme avec un code d’erreur, c’est à dire un nombre différent de 0 (succès) : 1, 2… exemple :
python3 script.py toto
code=$? #récupère le code de retour du programme
if (( $code != 0 )); then
bold=$(tput bold)
normal=$(tput sgr0)
echo -e "\n${bold}erreur : le programme a renvoyé ${code}${normal}"
fi
Exécution directe
Un programme en Python (ou dans un autre langage) peut-être exécuté sans
invoquer l’interpréteur sur la ligne de commande ; cf ./script.py … par
exemple. Pour cela, il faut :
- ajouter
#!/usr/bin/env python3en première ligne du script pour indiquer à l’interpréteur de commandes (shell) qu’il faut l’exécuter avec l’interpréteur mentionné (icipython3) ; - rendre le script exécutable
chmod u+x script.py.
Remarque : pour des raisons de sécurité, le système peut interdire d’exécuter des programmes présents sur une clé USB, sur un point de montage en réseau ou encore dans le répertoire personnel de l’utilisateur.
Un programme peut accéder au système de fichier : parcourir un dossier, vérifier si un fichier existe, renommer ou supprimer des fichiers… Cf module os de Python.
Vérifier si un fichier ou dossier existe
Les fonctions os.path.isfile(path: str) -> bool, os.path.isdir et os.path.exists
renvoient True si le chemin (path) est un fichier, un dossier ou simplement
si le chemin existe (quel que soit son type).
#!/usr/bin/env python3
import os
import sys
if __name__ == "__main__":
if len(sys.argv) != 2:
print("erreur : argument manquant", file=sys.stderr)
print("usage : " + sys.argv[0] + " chemin/dossier", file=sys.stderr)
sys.exit(1)
chemin = sys.argv[1]
if not os.path.exists(chemin): #fonctionne avec fichiers ou dossiers
print("erreur : " + chemin + " n'existe pas", file=sys.stderr)
sys.exit(2)
if not os.path.isdir(chemin): #isfile pour un fichier
print("erreur : " + chemin + " n'est pas un dossier", file=sys.stderr)
sys.exit(2)
print("ok : " + chemin + " existe et est un dossier")
*Rappel : “dir” siginifie “directory” (répertoire) ; c’est un synonyme de “folder” (dossier).
Remarque : dans cet exemple, des codes d’erreur différents sont utilisés : 1 pour paramètre(s) manquant(s), 2 pour fichier(s) / dossier(s) inexistant… c’est une bonne pratique, mais le choix est libre.
Lister un dossier
- La fonction
os.listdir(path: str) -> List[str]renvoie la liste des noms (sans le chemin) de fichiers (et sous-dossiers) d’un dossier (répertoire) ; elle déclenche des exceptions si le fichier n’existe pas, si ce n’est pas un dossier, si l’utilisateur n’a pas les permissions ou en cas d’erreur d’entrée-sortie. - La fonction
os.path.join(part1: str, part2: str, …) -> strpermet de concaténer deux (ou plusieurs) bouts de chemins en utilisant des\sous Windows et des/sous Linux et macOS.
Exemple
#!/usr/bin/env python3
import os
import sys
if __name__ == "__main__":
if len(sys.argv) != 2 or not os.path.isdir(sys.argv[1]):
print("usage : " + sys.argv[0] + " dossier", file=sys.stderr)
sys.exit(1 if len(sys.argv) != 2 else 2)
folder = sys.argv[1]
print("contenu du dossier " + folder + " :")
try:
for name in os.listdir(folder):
path = os.path.join(folder, name)
if os.path.isfile(path):
print(path)
else:
print(path + "/") #pour faire joli ;-)
except PermissionError:
print("erreur : accès refusé", file=sys.stderr)
except OSError as e: #autrefois IOError
print(f"erreur : {e}", file=sys.stderr)
Remarques
- L’existence du dossier a été vérifiée avant l’instruction
os.listdir(…); la capture des exceptionsFileNotFoundErroretNotADirectoryErrorn’est donc pas nécessaire. - Les erreurs d’entrées sorties ne peuvent pas être anticipées.
- Il est possible de vérifier les permissions au préalable : lecture (
os.R_OK), écriture (os.W_OK) ou accès / exécution (os.X_OK) ; exemple :
if not os.access(folder, os.R_OK):
print("erreur : accès refusé", file=sys.stderr)
sys.exit(3) #bonne pratique: autre cas d'erreur, autre code
Pour travailler sur un fichier, il faut au préalable l’ouvrir, le parcourir (s’il est en lecture), ou y écrire, puis le fermer.
- La fonction pour ouvrir un fichier est
open(chemin: str, mode: str); elle prend en paramètre le chemin du fichier et le mode d’ouverture :r(ead) /w(rite),t(text) /b(binary) et renvoie un pointeur de fichier (dont le type précis varie selon le mode). - Les fonctions de lecture sont
readline()etread(nb: int), qui prend en paramètre le nombre de charactères (texte) ou d’octets (binaire) à lire ; elles renvoient du texte (str) ou des octets (bytes). - La fonction d’écriture est
write(content: str/bytes). - La fonction pour fermer un fichier est
close(); la syntaxe avecwithpermet de s’en affranchir (fermeture automatique à la sortie du bloc).
Les exemples présentés ici sont un minimum pour lire et écrire dans des fichiers textes ou binaires. Pour approfondir, se référer à la documentation officielle.
Lecture de texte
La fonction readline() lit une ligne entière — jusqu’au caractère de fin
de ligne (\n et/ou \r). Attention, les caractères de fin de ligne sont
inclus dans la valeur renvoyée ; la fonction strip() permet de les supprimer.
Exemple :
#!/usr/bin/env python3
import sys
try:
f = open("test.txt", "rt")
line = f.readline()
i = 1
while line != "":
print(str(i).ljust(2) + " : " + line.strip("\n"))
line = f.readline()
i += 1
f.close()
except FileNotFoundError:
print("erreur : le fichier n'existe pas", file=sys.stderr)
except PermissionError:
print("erreur : accès refusé", file=sys.stderr)
except OSError as e:
print("erreur : " + str(e), file=sys.stderr)
Remarque: la variable i n’a qu’un rôle pédagogique et pourrait être omise.
Syntaxe avec “with" :
with open("test.txt", "rt") as f:
line = f.readline()
i = 1
while line != "":
print(str(i).ljust(2) + " : " + line.strip())
line = f.readline()
i += 1
#le close peut être omis
Syntaxe compacte :
with open("test.txt", "rt") as f:
while "" != (line := f.readline()):
print(line.strip().upper()) #avec mise en majuscules
Écriture texte
L’écriture dans un fichier texte peut s’effectuer ligne par ligne ; Penser à ajouter les caractères de fins de ligne.
with open("test.txt", "wt") as f:
line = input("Saisir une ligne : ")
while "" != line:
f.write(line + "\n")
line =input("Saisir une ligne : ")
Remarque pour le professeur : “Basthon Notebook -> Fichier -> Ouvrir”, et
import basthon; basthon.download("test.txt").
Écriture binaire
L’écriture binaire demande un peu de rigueur : contrairement aux lignes d’un fichier texte qui peuvent être de longueur variables, il n’est pas possible d’utiliser un délimiteur dans le cas de données binaires.
Dans le cas de nombres, il est par exemple nécessaire de les écrire sur un nombre déterminé d’octets (1, 2, 4 ou 8) — selon le nombre de bits nécessaires pour représenter la plus grande valeur. Il y a plusieurs possibilités pour obtenir la séquence d’octets (bytes) représentant un nombre :
- par le calcul (base 256)
- utiliser la méthode
to_bytes(nb_octets: int)sur un nombre entier ; - utiliser la fonction
struct.pack(format: str, valeur); les formats sont :- préfixe
<pour ordonner les octets en petit boutiste (“little-endian”) — par défaut sur architecture “x86” / “x86_64” ou>pour les ordonner en grand boutiste (les octets de poids fort en premier) ; bpour un nombre signés codé sur 8 bits (de -128 à + 127) ;Bpour un nombre non signé sur 8 bits (de 0 à 255) ;h/Hpour des nombres signés / non signés sur 16 bits ;i/Ipour des nombres signés / non signés sur 32 bits ;q/Qpour des nombres signés / non signés sur 64 bits.
- préfixe
import struct
with open("test.bin", "wb") as f:
nbr = input("Saisir un nombre entre 0 et 65535 : ")
while "" != nbr:
#b_nbr = bytes([nbr//256, nbr%256]) #calcul : grand boutiste
#b_nbr = int(nbr).to_bytes(2) #grand boutiste
b_nbr = struct.pack(">H", int(nbr))
f.write(b_nbr)
nbr = input("Saisir un nombre entre 0 et 65535 : ")
La commande hexdump -X test.bin permet de visualiser le contenu du fichier
généré. Comparer le résultat selon les formats petit et grand boutiste.
Lecture binaire
Pour la lecture d’un fichier binaire, il faut être également être rigoureux,
notamment pour la lecture des nombres. Le programme suivant utilise la fonction
struct.unpack pour décoder les octets lus ; le résultat devrait être le
même que celui obtenu par le calcul (base 256).
import struct
with open("test.bin", "rb") as f:
while len(b_nbr := f.read(2)) != 0:
u_nbr = struct.unpack(">H", b_nbr)[0] #unpack renvoie un tuple
nbr = b_nbr[0]*256 + b_nbr[1] #grand boutiste
#nbr = b_nbr[1]*256 + b_nbr[0] #petit boutiste
print(b_nbr, u_nbr, nbr)
Le choix entre petit et grand boutiste doit être conforme à celui utilisé pour l’écriture.
Approfondissement / réflexion (rappel mathématiques) : soit “b0” une séquence
binaire (“101” par exemple) ; “b1” est cette même séquence décalée d’un bit
vers la gauche (on ajoute un “0” à droite : b1 = b0<<1 = “1010”) ; indiquer
à quelles opérations correspondent un décalage de “n” bits vers la gauche
(ou la droite).