153 votes

Comment puis-je exécuter une commande externe de manière asynchrone depuis Python ?

Je dois exécuter une commande shell de manière asynchrone à partir d'un script Python. Je veux dire par là que je veux que mon script Python continue à s'exécuter pendant que la commande externe part faire ce qu'elle doit faire.

J'ai lu ce post :

Appel d'une commande externe en Python

J'ai ensuite fait quelques tests, et on dirait que os.system() fera l'affaire à condition que j'utilise & à la fin de la commande pour ne pas avoir à attendre qu'elle revienne. Je me demande si c'est la bonne façon d'accomplir une telle chose ? J'ai essayé commands.call() mais cela ne fonctionne pas pour moi car cela bloque sur la commande externe.

Faites-moi savoir si vous utilisez os.system() si cela est conseillé ou si je dois essayer une autre voie.

160voto

Ali Afshar Points 22836

subprocess.Popen fait exactement ce que vous voulez.

from subprocess import Popen
p = Popen(['watch', 'ls']) # something long running
# ... do other stuff while subprocess is running
p.terminate()

(Modifier pour compléter la réponse des commentaires)

L'instance Popen peut faire diverses autres choses comme vous pouvez poll() pour voir s'il est toujours en marche, et vous pouvez communicate() avec lui pour lui envoyer des données sur stdin, et attendre qu'il se termine.

5 votes

Vous pouvez également utiliser poll() pour vérifier si le processus enfant s'est terminé, ou utiliser wait() pour attendre qu'il se termine.

0 votes

Adam, c'est tout à fait vrai, bien qu'il soit préférable d'utiliser communicate() pour attendre, car cette méthode gère mieux les tampons d'entrée et de sortie et il existe des situations où l'inondation de ces tampons peut bloquer.

0 votes

Adam : la documentation dit "Attention Ceci va provoquer un blocage si le processus enfant génère suffisamment de sortie vers un tube stdout ou stderr pour qu'il se bloque en attendant que le tampon du tube OS accepte plus de données. Utilisez communicate() pour éviter cela. "

58voto

cdleary Points 18869

Si vous souhaitez exécuter de nombreux processus en parallèle et les traiter lorsqu'ils produisent des résultats, vous pouvez utiliser la polling comme dans l'exemple suivant :

from subprocess import Popen, PIPE
import time

running_procs = [
    Popen(['/usr/bin/my_cmd', '-i %s' % path], stdout=PIPE, stderr=PIPE)
    for path in '/tmp/file0 /tmp/file1 /tmp/file2'.split()]

while running_procs:
    for proc in running_procs:
        retcode = proc.poll()
        if retcode is not None: # Process finished.
            running_procs.remove(proc)
            break
        else: # No process is done, wait a bit and check again.
            time.sleep(.1)
            continue

    # Here, `proc` has finished with return code `retcode`
    if retcode != 0:
        """Error handling."""
    handle_results(proc.stdout)

Le flux de contrôle est un peu alambiqué parce que j'essaie de le rendre petit - vous pouvez le remanier à votre goût :-)

Cela présente l'avantage de servir d'abord les demandes les plus précoces. Si vous appelez communicate sur le premier processus en cours d'exécution et qui s'avère être le plus long, les autres processus en cours d'exécution seront restés inactifs alors que vous auriez pu traiter leurs résultats.

3 votes

@Tino Cela dépend de la façon dont vous définissez busy-wait. Voir Quelle est la différence entre busy-wait et polling ?

1 votes

Existe-t-il un moyen d'interroger un ensemble de processus et pas seulement un seul ?

1 votes

Note : il peut se bloquer si un processus génère suffisamment de sortie. Vous devriez consommer stdout simultanément si vous utilisez PIPE (il y a (trop mais pas assez) d'avertissements dans la documentation du sous-processus à ce sujet).

28voto

gerrit Points 1588

Ceci est couvert par Exemples de sous-processus Python 3 sous "Attendre que la commande se termine de manière asynchrone". Exécutez ce code en utilisant IPython o python -m asyncio :

import asyncio

proc = await asyncio.create_subprocess_exec(
   'ls','-lha',
   stdout=asyncio.subprocess.PIPE,
   stderr=asyncio.subprocess.PIPE)

# do something else while ls is working

# if proc takes very long to complete, the CPUs are free to use cycles for 
# other processes
stdout, stderr = await proc.communicate()

Le processus commencera à fonctionner dès que le await asyncio.create_subprocess_exec(...) est terminée. S'il n'est pas terminé au moment où vous appelez await proc.communicate() il attendra là pour vous donner le statut de votre sortie. Si elle est terminée, proc.communicate() reviendra immédiatement.

L'essentiel ici est similaire à Réponse de Terrels mais je pense que la réponse de Terrels semble surcompliquer les choses.

Voir asyncio.create_subprocess_exec pour plus d'informations.

0 votes

La limite est que Python 3.6+ est nécessaire.

4 votes

@DanielF Vrai, mais tout Python plus ancien que la version 3.6 n'est plus supporté, donc tout le monde devrait déjà utiliser au moins Python 3.6.

0 votes

J'ai obtenu cette erreur proc = await asyncio.create_subprocess_exec( SyntaxError: 'await' outside function

14voto

S.Lott Points 207588

Ce que je me demande, c'est si [os.system()] est la bonne façon d'accomplir une telle chose ?

Non. os.system() n'est pas la bonne méthode. C'est pourquoi tout le monde dit qu'il faut utiliser subprocess .

Pour plus d'informations, lisez http://docs.python.org/library/os.html#os.system

Le module de sous-processus fournit plus plus puissants pour créer de nouveaux nouveaux processus et la récupération de leurs résultats ; l'utilisation de ce module est préférable à l'utilisation de cette fonction. Utilisez le module subprocess. Consultez particulièrement la section Remplacement des anciennes fonctions avec le module subprocess la section.

0 votes

NON car os.system() est synchrone et non asynchrone

0 votes

@user889030 Comme je le comprends, os.system("my_command &") exécutera la commande en arrière-plan de manière asynchrone.

9voto

Terrel Shumway Points 331

La réponse acceptée est muy vieux.

J'ai trouvé une meilleure réponse moderne ici :

https://kevinmccarthy.org/2016/07/25/streaming-subprocess-stdin-and-stdout-with-asyncio-in-python/

et fait quelques changements :

  1. le faire fonctionner sous Windows
  2. le faire fonctionner avec des commandes multiples

    import sys import asyncio

    if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())

    async def _read_stream(stream, cb): while True: line = await stream.readline() if line: cb(line) else: break

    async def _stream_subprocess(cmd, stdout_cb, stderr_cb): try: process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE )

        await asyncio.wait(
            [
                _read_stream(process.stdout, stdout_cb),
                _read_stream(process.stderr, stderr_cb),
            ]
        )
        rc = await process.wait()
        return process.pid, rc
    except OSError as e:
        # the program will hang if we let any exception propagate
        return e

    def execute(aws): """ run the given coroutines in an asyncio loop returns a list containing the values returned from each coroutine. """ loop = asyncio.get_event_loop() rc = loop.run_until_complete(asyncio.gather(aws)) loop.close() return rc

    def printer(label): def pr(*args, *kw): print(label, args, **kw)

    return pr

    def name_it(start=0, template="s{}"): """a simple generator for task names """ while True: yield template.format(start) start += 1

    def runners(cmds): """ cmds is a list of commands to excecute as subprocesses each item is a list appropriate for use by subprocess.call """ next_name = name_it().next for cmd in cmds: name = next_name() out = printer(f"{name}.stdout") err = printer(f"{name}.stderr") yield _stream_subprocess(cmd, out, err)

    if name == "main": cmds = ( [ "sh", "-c", """echo "$SHELL"-stdout && sleep 1 && echo stderr 1>&2 && sleep 1 && echo done""", ], [ "bash", "-c", "echo 'hello, Dave.' && sleep 1 && echo dave_err 1>&2 && sleep 1 && echo done", ], [sys.executable, "-c", 'print("hello from python");import sys;sys.exit(2)'], )

    print(execute(*runners(cmds)))

Il est peu probable que les commandes de l'exemple fonctionnent parfaitement sur votre système, et il ne gère pas les erreurs bizarres, mais ce code démontre une façon d'exécuter plusieurs sous-processus en utilisant asyncio et de diffuser la sortie.

0 votes

Je l'ai testé avec cpython 3.7.4 sous Windows et cpython 3.7.3 sous Ubuntu WSL et Alpine Linux natif.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X