Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Lire et modifier un script Python qu'on n'a pas écrit

Par mis à jour le

Améliorer cette page

Pour reprendre un script Python que personne n'a écrit dans l'équipe, ne commencez pas par le lire de haut en bas. Lisez sa docstring pour le rôle, descendez au bloc if __name__ == "__main__" qui est son point d'entrée, notez ses options et ses variables d'environnement, lancez-le sans risque, regardez ce qu'il affiche et son code de sortie, puis trouvez la fonction qui décide. Ce n'est qu'ensuite qu'on le modifie, d'un caractère si possible, et qu'on relance pour vérifier. Cette leçon conclut le module de découverte de Python : elle utilise ce que vous avez appris (variables, conditions, boucles, fonctions, fichiers, exceptions) sur un petit script d'exploitation, puis le lab vous confie certcheck.py.

  • Dire ce que fait un script avant d'en lire le détail.
  • Repérer son point d'entrée, ses options et ses variables d'environnement.
  • Voir quel programme externe il lance et ce qu'il affiche.
  • Lire son code de sortie et le sens qu'il porte.
  • Trouver la fonction qui décide, puis la modifier et vérifier.

Aucune autre notion de Python n'est supposée : la leçon montre comment lire un script, pas comment en écrire un de zéro.

Un script hérité arrive avec une phrase du type « il tourne chaque nuit, ne le touchez pas ». Lire du code ressemble à une enquête : on pose des questions précises, et on cherche la réponse dans le code ou dans son exécution. Cinq questions, dans cet ordre, suffisent pour un script : que fait-il, par où démarre-t-il, de quoi dépend-il, que produit-il, où décide-t-il ? Tant que vous n'avez pas répondu, ne modifiez rien : une correction faite sur une mauvaise compréhension ajoute un défaut de plus.

Pour la démonstration, voici archivelog.py, un script d'une trentaine de lignes qui compresse les vieux journaux d'un dossier. Le lab applique la même démarche à certcheck.py. Pour suivre les essais, enregistrez-le dans un fichier archivelog.py. Lisez-le en entier une fois, sans chercher à tout comprendre : les sections suivantes le décortiquent.

#!/usr/bin/env python3
"""archivelog : compresse les journaux .log d'un dossier.
Usage : python3 archivelog.py --dossier /var/log/appli --age 7
"""
import argparse
import os
import subprocess
import sys
import time
def trop_vieux(age_en_jours, seuil):
"""Vrai si le fichier a plus de `seuil` jours."""
return age_en_jours > seuil
def main():
parser = argparse.ArgumentParser(description="Compresse les vieux journaux.")
parser.add_argument("--dossier", default=os.environ.get("ARCHIVELOG_DOSSIER", "journaux"))
parser.add_argument("--age", type=int, default=7, help="âge minimal, en jours")
args = parser.parse_args()
compresses = 0
for nom in sorted(os.listdir(args.dossier)):
if not nom.endswith(".log"):
continue
chemin = os.path.join(args.dossier, nom)
age = int((time.time() - os.path.getmtime(chemin)) / 86400)
if trop_vieux(age, args.age):
subprocess.run(["gzip", chemin], check=True)
print(f"compressé : {nom}")
compresses += 1
return 0 if compresses else 3
if __name__ == "__main__":
sys.exit(main())

Le rôle d'un script se lit d'abord dans sa docstring, la chaîne entre triples guillemets placée tout en haut : ici, « compresse les journaux .log d'un dossier », avec un exemple d'appel. Une docstring peut être périmée, donc on la traite comme une déclaration d'intention, à confirmer par la lecture et par l'exécution.

Le point d'entrée est l'endroit où l'exécution commence quand on lance le fichier. En Python, on le cherche tout en bas : le bloc if __name__ == "__main__": ne s'exécute que lorsque le fichier est lancé directement, et non lorsqu'un autre fichier l'importe. Ici, il appelle main() et passe son résultat à sys.exit, ce qui fait de ce nombre le code de sortie du script. Lisez ensuite main() de haut en bas : c'est elle qui appelle les autres fonctions, et c'est donc elle la table des matières.

Quelles options, variables et programmes externes ?

Section intitulée « Quelles options, variables et programmes externes ? »

Un script se règle de l'extérieur, et ces réglages sont les entrées à noter en premier. Trois endroits à repérer dans main() : argparse déclare les options de la ligne de commande, os.environ.get(...) lit une variable d'environnement (une paire nom-valeur que le shell transmet au programme), et subprocess.run(...) lance un programme externe, ici gzip. Python n'a pas à tout faire : il pilote des outils du système.

Le plus simple pour lister les options est de demander l'aide au script lui-même, car argparse la génère à partir de ses déclarations :

Fenêtre de terminal
python3 archivelog.py --help

La sortie liste --dossier, --age et l'option -h. Une valeur par défaut cachée dans le code se lit dans l'argument default= : ici, le dossier vaut la variable ARCHIVELOG_DOSSIER si elle existe, sinon journaux. Pour ne rien oublier, un grep relève tous les indices d'un coup, mais il propose des candidats : il ne prouve jamais qu'il n'y a rien d'autre.

Fenêtre de terminal
grep -nE "argparse|os\.environ|subprocess|open\(" archivelog.py

Exécuter est la meilleure façon de vérifier ce que vous avez cru lire, à condition de ne pas toucher au réel. Trois précautions couvrent la plupart des cas : travailler sur une copie jetable des fichiers, pointer le script vers des données d'essai plutôt que vers la production, et s'assurer que le script ne fait que lire quand c'est possible. Ici, archivelog.py compresse des fichiers : l'essai se fait dans un dossier créé pour l'occasion.

Fenêtre de terminal
mkdir essai && printf 'a\n' > essai/vieux.log && printf 'b\n' > essai/neuf.log
touch -d '10 days ago' essai/vieux.log
ARCHIVELOG_DOSSIER=essai python3 archivelog.py
echo "code : $?"
ls essai

La sortie affiche compressé : vieux.log, puis code : 0, puis la liste neuf.log et vieux.log.gz. La syntaxe VARIABLE=valeur commande fixe la variable pour cette seule exécution. Comparez le résultat à ce que vous aviez lu : si un fichier imprévu apparaît ou disparaît, la lecture était fausse et il faut la corriger.

Le code de sortie est le nombre qu'un programme rend à celui qui l'a lancé. 0 veut dire « tout s'est bien passé », tout autre nombre signale quelque chose, et echo $? affiche celui de la dernière commande. C'est le seul élément que lisent cron (le planificateur de tâches de Linux) ou un pipeline : il décide s'il doit prévenir quelqu'un. Dans le script, c'est la valeur que rend main(), ici return 0 if compresses else 3.

Relancez le script une seconde fois dans le même dossier : plus rien n'est à compresser, la sortie est vide et echo "code : $?" affiche cette fois code : 3. Le script distingue donc « j'ai travaillé » de « il n'y avait rien à faire », ce qu'aucun texte affiché ne dirait aussi nettement. Lire ce que produit un script, c'est lire les deux : ce qu'il imprime et le code qu'il rend. Les messages d'erreur, eux, passent par un flux séparé, la sortie d'erreur (stderr).

Dans la plupart des scripts d'exploitation, une seule fonction porte la règle métier : celle qui répond à la question « faut-il agir ? ». Elle est courte, prend des valeurs en entrée et rend un verdict. Ici, c'est trop_vieux(age_en_jours, seuil). Pour la trouver, partez de main() et cherchez, à l'intérieur de la boucle, la condition if qui commande l'action : la fonction qu'elle appelle est la bonne.

C'est aussi là que se cachent les cas limites, c'est-à-dire les valeurs exactement à la frontière. Avec age_en_jours > seuil, un fichier de exactement 7 jours n'est pas compressé avec --age 7 ; avec age_en_jours >= seuil, il l'est. Les deux écritures sont correctes, mais elles ne disent pas la même chose, et une documentation qui parle d'« au moins 7 jours » ne s'accorde qu'avec la seconde. Les défauts d'un script hérité se rangent presque toujours dans trois familles : un cas limite mal tranché, une erreur avalée par un except qui continue sans rien dire, ou un secret écrit en clair dans le code. Chacun laisse un signe qu'une lecture attentive retrouve, mais un défaut n'est avéré qu'une fois reproduit.

On ne modifie qu'après avoir compris, et on modifie le moins possible : un caractère de condition, un cas ajouté. La méthode tient en quatre gestes. Observez d'abord le comportement actuel avec une commande précise, changez une seule chose, relancez exactement la même commande, puis comparez les deux résultats. Si le résultat n'est pas celui que vous attendiez, annulez et relisez : ne corrigez pas par essais successifs.

Voici le cycle sur un cas limite. Le dossier d'essai doit contenir vieux.log non encore compressé : l'essai précédent l'a compressé, donc recréez-le d'abord, puis touch -d '7 days ago' essai/vieux.log, qui le rend vieux de 7 jours révolus (le script compte des jours entiers : int(...) ignore les heures restantes). On lance ensuite avec --age 7. Avant la modification, age_en_jours > seuil ne le compresse pas, car 7 n'est pas supérieur à 7. On change > en >= dans trop_vieux, on relance la même commande, et le fichier est cette fois compressé. La différence entre les deux sorties prouve que votre modification fait ce que vous croyez. Pour garder une trace de ce que vous changez et pouvoir l'annuler, Git est l'outil de référence : la formation l'introduit avec le travail collaboratif, et la formation Git montre aussi comment retrouver, dans l'historique, qui a changé une ligne et pourquoi.

Le lab vous confie certcheck.py, un script d'environ quatre-vingts lignes qu'un collègue a écrit puis quitté, et qui contrôle chaque nuit l'expiration des certificats TLS du parc. Vous le lisez d'abord sans le modifier : son rôle, l'option qui règle le seuil, la variable qui remplace la date du jour, le programme qu'il lance, la fonction qui décide, puis ce qu'il affiche et son code de sortie à une date fixée. Vous le modifiez ensuite : le seuil devient inclusif, et un certificat déjà expiré s'affiche expired. La validation porte sur vos réponses dans reponses.yaml, que le contrôle recalcule, et sur le comportement du script modifié, rejoué sur des cas tirés au hasard.

  • Lire un script est une enquête : rôle, point d'entrée, entrées, sorties, fonction qui décide, dans cet ordre, avant toute modification.
  • La docstring donne le rôle annoncé, et le bloc if __name__ == "__main__" donne le point d'entrée, qui appelle main().
  • Les options viennent d'argparse (et de --help), les variables d'environnement d'os.environ, les programmes externes de subprocess.
  • On lance un script inconnu sur des données d'essai, en notant ce qu'il affiche et son code de sortie (echo $?).
  • La fonction qui décide est celle que la boucle appelle pour savoir s'il faut agir : c'est là que se jouent les cas limites, comme > contre >=.
  • Une modification se fait petite, se vérifie en relançant la même commande et en comparant les deux résultats.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn