Comment utiliser le module subprocess pour exécuter des programmes externes en Python 3

L'auteur a choisi le COVID-19 Relief Fund pour recevoir un don dans le cadre du programme Write for DOnations.

Introduction

Python 3 comprend le module subprocess permettant d'exécuter des programmes externes et de lire leurs sorties dans votre code Python.

Il se peut que vous trouviez subprocess utile si vous voulez utiliser un autre programme sur votre ordinateur à partir de votre code Python. Par exemple, vous pouvez invoquer git depuis votre code Python pour récupérer les fichiers de votre projet qui sont suivis dans le contrôle de version de git. Comme tout programme auquel vous pouvez accéder sur votre ordinateur par subprocess, les exemples présentés ici s'appliquent à tout programme externe que vous pourriez vouloir invoquer à partir de votre code Python.

subprocess comprend plusieurs classes et fonctions, mais dans ce tutoriel, nous couvrirons l'une des fonctions les plus utiles de subprocess : subprocess.run. Nous passerons en revue ses différentes utilisations et les principaux arguments des mots-clés.

Conditions préalables

Pour tirer le meilleur parti de ce tutoriel, il est recommandé d'être familiarisé avec la programmation en Python 3. Vous pouvez consulter ces tutoriels pour obtenir les informations de base nécessaires :

  • Comment coder en Python 3

Exécution d'un programme externe

Vous pouvez utiliser la fonction subprocess.run pour exécuter un programme externe à partir de votre code Python. Mais d'abord, vous devez importer les modules subprocess et sys dans votre programme :

import subprocess import sys  result = subprocess.run([sys.executable, "-c", "print('ocean')"]) 

Si vous l'exécutez, vous obtiendrez une sortie comme ci-dessous :

Outputocean 

Passons en revue cet exemple :

  • sys.executable est le chemin absolu vers l'exécutable Python avec lequel votre programme a été invoqué à l'origine. Par exemple, sys.executable pourrait être un chemin tel que /usr/local/bin/python.
  • subprocess.run reçoit une liste de chaînes de caractères comprenant les composants de la commande que nous essayons d'exécuter. Comme la première chaîne que nous passons est sys.executable, nous ordonnons à subprocess.run d'exécuter un nouveau programme Python.
  • Le composant -c est une option de ligne de commande python qui vous permet de passer une chaîne avec un programme Python entier à exécuter. Dans notre cas, nous passons un programme qui imprime la chaîne ocean.

Vous pouvez penser que chaque entrée de la liste que nous passons à subprocess.run est séparée par un espace. Par exemple, [sys.executable, "-c", "print('ocean')"] se traduit approximativement par /usr/local/bin/python -c "print('ocean')". Notez que subprocess cite automatiquement les composants de la commande avant d'essayer de les exécuter sur le système d'exploitation sous-jacent de sorte que, par exemple, vous pouvez passer un nom de fichier qui contient des espaces.

Warning : ne jamais transmettre une entrée non fiable à subprocess.run. Comme subprocess.run a la capacité d'exécuter des commandes arbitraires sur votre ordinateur, des acteurs malveillants peuvent l'utiliser pour manipuler votre ordinateur de manière inattendue.

Capturer output d'un programme externe

Maintenant que nous pouvons invoquer un programme externe en utilisant subprocess.run, voyons comment nous pouvons récupérer la sortie de ce programme. Par exemple, ce processus pourrait être utile si nous voulions utiliser git ls-files pour produire tous vos fichiers actuellement stockés sous contrôle de version.

Note : Les exemples présentés dans cette section nécessitent Python 3.7 ou version supérieure. En particulier, les arguments capture_output et le mot-clé text ont été ajoutés à Python 3.7 lors de sa sortie en juin 2018.

Ajoutons à notre exemple précédent :

import subprocess import sys  result = subprocess.run(     [sys.executable, "-c", "print('ocean')"], capture_output=True, text=True ) print("stdout:", result.stdout) print("stderr:", result.stderr) 

Si nous exécutons ce code, nous obtiendrons le résultat suivant :

Outputstdout: ocean  stderr: 

Cet exemple est en grande partie le même que celui présenté dans la première section : nous sommes toujours en train d'exécuter un sous-processus pour imprimer ocean. Il est toutefois important de noter que nous passons les arguments des mots-clés capture_output=True et text=True à subprocess.run.

subprocess.run renvoie un objet subprocess.CompletedProcess qui est lié à result. L'objet subprocess.CompletedProcess comprend des détails sur le code de sortie du programme externe et sa sortie. capture_output=True garantit que result.stdout et result.stderr sont remplis avec la sortie correspondante du programme externe. Par défaut, result.stdout et result.stderr sont liés en tant qu'octets, mais l'argument du mot-clé text=True indique à Python de décoder plutôt les octets en chaînes de caractères.

Dans la section de sortie, stdout est ocean (plus la nouvelle ligne de fin que print ajoute implicitement), et nous n'avons pas de stderr.

Essayons un exemple qui produit une valeur non vide pour stderr :

import subprocess import sys  result = subprocess.run(     [sys.executable, "-c", "raise ValueError('oops')"], capture_output=True, text=True ) print("stdout:", result.stdout) print("stderr:", result.stderr) 

Si nous exécutons ce code, nous obtenons une sortie comme celle qui suit :

Outputstdout: stderr: Traceback (most recent call last):   File "<string>", line 1, in <module> ValueError: oops  

Ce code exécute un sous-processus Python qui génère immédiatement une ValueError. Lorsque nous inspectons le result final, nous ne voyons rien dans stdout et un Traceback de notre ValueError dans stderr. C'est parce que par défaut Python écrit le Traceback de l'exception non gérée à stderr .

Lever une exception sur un code de sortie incorrect

Il est parfois utile de lever une exception si un programme que nous exécutons sort avec un code de sortie incorrect. Les programmes qui sortent avec un code nul sont considérés comme réussis, mais les programmes qui sortent avec un code non-nul sont considérés comme ayant rencontré une erreur. Par exemple, ce modèle pourrait être utile si nous voulions lever une exception dans le cas où nous exécutons des fichiers git ls-files dans un répertoire qui n'est pas réellement un référentiel git.

Nous pouvons utiliser l'argument check=True du mot-clé subprocess.run pour qu'une exception soit levée si le programme externe renvoie un code de sortie non-nul :

import subprocess import sys  result = subprocess.run([sys.executable, "-c", "raise ValueError('oops')"], check=True) 

Si nous exécutons ce code, nous obtenons une sortie comme celle qui suit :

OutputTraceback (most recent call last):   File "<string>", line 1, in <module> ValueError: oops Traceback (most recent call last):   File "<stdin>", line 1, in <module>   File "/usr/local/lib/python3.8/subprocess.py", line 512, in run     raise CalledProcessError(retcode, process.args, subprocess.CalledProcessError: Command '['/usr/local/bin/python', '-c', "raise ValueError('oops')"]' returned non-zero exit status 1. 

Cette sortie montre que nous avons exécuté un sous-processus qui a généré une erreur imprimée en stderr dans notre terminal. Ensuite, subprocess.run a consciencieusement levé un subprocess.CalledProcessError en notre nom dans notre programme Python principal.

Alternativement, le module de sous-processus comprend également la méthode subprocess.CompletedProcess.check_returncode que nous pouvons invoquer pour un effet similaire :

import subprocess import sys  result = subprocess.run([sys.executable, "-c", "raise ValueError('oops')"]) result.check_returncode() 

Si nous exécutons ce code, nous recevrons :

OutputTraceback (most recent call last):   File "<string>", line 1, in <module> ValueError: oops Traceback (most recent call last):   File "<stdin>", line 1, in <module>   File "/usr/local/lib/python3.8/subprocess.py", line 444, in check_returncode     raise CalledProcessError(self.returncode, self.args, self.stdout, subprocess.CalledProcessError: Command '['/usr/local/bin/python', '-c', "raise ValueError('oops')"]' returned non-zero exit status 1. 

Comme nous n'avons pas passé check=True à subprocess.run, nous avons lié avec succès une instance de subprocess.CompletedProcess à result, même si notre programme s'est terminé avec un code non nul. L'appel de result.check_returncode() fait cependant apparaître un sous-processus appelé CalledProcessError parce qu'il détecte le processus terminé sorti avec un code incorrect.

Utilisation du délai d'attente pour quitter prématurément les programmes

subprocess.run inclut l'argument timeout pour vous permettre d'arrêter un programme externe si son exécution est trop longue :

import subprocess import sys  result = subprocess.run([sys.executable, "-c", "import time; time.sleep(2)"], timeout=1) 

Si nous exécutons ce code, nous obtiendrons le résultat suivant :

OutputTraceback (most recent call last):   File "<stdin>", line 1, in <module>   File "/usr/local/lib/python3.8/subprocess.py", line 491, in run     stdout, stderr = process.communicate(input, timeout=timeout)   File "/usr/local/lib/python3.8/subprocess.py", line 1024, in communicate     stdout, stderr = self._communicate(input, endtime, timeout)   File "/usr/local/lib/python3.8/subprocess.py", line 1892, in _communicate     self.wait(timeout=self._remaining_time(endtime))   File "/usr/local/lib/python3.8/subprocess.py", line 1079, in wait     return self._wait(timeout=timeout)   File "/usr/local/lib/python3.8/subprocess.py", line 1796, in _wait     raise TimeoutExpired(self.args, timeout) subprocess.TimeoutExpired: Command '['/usr/local/bin/python', '-c', 'import time; time.sleep(2)']' timed out after 0.9997982999999522 seconds 

Le sous-processus que nous avons essayé d'exécuter utilisait la fonction time.sleep pour se mettre en veille pendant 2 secondes. Cependant, nous avons passé l'argument du mot-clé timeout=1 à subprocess.run pour que notre sous-processus soit temporisé après 1 seconde. Cela explique pourquoi notre appel à subprocess.run a finalement soulevé une exception de subprocess.TimeoutExpired.

Notez que l'argument du mot-clé timeout à subprocess.run est approximatif. Python fera tout son possible pour arrêter le sous-processus après le nombre de secondes stipulé dans timeout, mais ce ne sera pas nécessairement exact.

Transmission d'Input aux programmes

Parfois, les programmes s'attendent à ce que les données leur soient transmises via stdin.

L'argument du mot-clé input à subprocess.run vous permet de passer des données au stdin du sous-processus. Par exemple :

import subprocess import sys  result = subprocess.run(     [sys.executable, "-c", "import sys; print(sys.stdin.read())"], input=b"underwater" ) 

Après l'exécution de ce code, nous recevrons une sortie comme celle qui suit :

Outputunderwater 

Dans ce cas, nous avons passé les octets underwater à input. Notre sous-processus cible a utilisé sys.stdin pour lire le passage dans stdin ( underwater ) et l'a imprimé dans notre sortie.

L'argument du mot-clé input peut être utile si vous voulez enchaîner plusieurs appels subprocess.run ensemble en passant la sortie d'un programme comme entrée à un autre.

Conclusion

Le module subprocess est une partie puissante de la bibliothèque standard Python, qui vous permet d'exécuter des programmes externes et d'inspecter leurs sorties facilement. Dans ce tutoriel, vous avez appris à utiliser subprocess.run pour contrôler des programmes externes, leur transmettre des entrées, analyser leurs sorties et vérifier leurs codes de retour.

Le module subprocess propose des classes et des utilitaires supplémentaires que nous n'avons pas abordés dans ce tutoriel. Maintenant que vous disposez d'une base de référence, vous pouvez utiliser la documentation du module subprocess pour en savoir plus sur d'autres classes et utilitaires disponibles.