Come posso eseguire un comando esterno in modo asincrono da Python?


120

Ho bisogno di eseguire un comando di shell in modo asincrono da uno script Python. Con questo intendo che voglio che il mio script Python continui a funzionare mentre il comando esterno si spegne e fa tutto ciò che deve fare.

Ho letto questo post:

Chiamare un comando esterno in Python

Quindi sono uscito e ho fatto alcuni test, e sembra che os.system()farà il lavoro a condizione che lo uso &alla fine del comando in modo da non dover aspettare che ritorni. Quello che mi chiedo è se questo è il modo corretto per realizzare una cosa del genere? Ho provato commands.call()ma non funzionerà per me perché si blocca sul comando esterno.

Per favore fatemi sapere se l'utilizzo os.system()per questo è consigliabile o se dovrei provare un altro percorso.

Risposte:


135

subprocess.Popen fa esattamente quello che vuoi.

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

(Modifica per completare la risposta dai commenti)

L'istanza Popen può fare varie altre cose come puoi poll()vedere se è ancora in esecuzione, e puoi communicate()con essa inviarle dati su stdin e attendere che termini.


4
Puoi anche usare poll () per controllare se il processo figlio è terminato, o usare wait () per aspettare che termini.
Adam Rosenfield

Adam, molto vero, anche se potrebbe essere meglio usare communic () per aspettare perché ha una migliore gestione dei buffer in / out e ci sono situazioni in cui il flooding di questi potrebbe bloccarsi.
Ali Afshar

Adam: i documenti dicono "Avviso Questo si bloccherà se il processo figlio genera un output sufficiente per una pipe stdout o stderr in modo tale da bloccare l'attesa che il buffer del pipe del sistema operativo accetti più dati. Usa communic () per evitarlo."
Ali Afshar

14
communic () e wait () sono operazioni di blocco, però. Non parallelizzerai i comandi come sembra chiedere all'OP se li usi.
cdleary

1
Cdleary è assolutamente corretto, va detto che comunica e aspetta blocca, quindi fallo solo quando aspetti che le cose si chiudano. (Cosa che dovresti davvero fare per essere ben educato)
Ali Afshar

48

Se vuoi eseguire molti processi in parallelo e poi gestirli quando producono risultati, puoi usare il polling come nel seguente:

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)

Il flusso di controllo è un po 'contorto perché sto cercando di renderlo piccolo: puoi refactoring secondo i tuoi gusti. :-)

Questo ha il vantaggio di soddisfare prima le richieste di finitura anticipata. Se si richiama communicateil primo processo in esecuzione e questo risulta essere il più lungo, gli altri processi in esecuzione saranno rimasti inattivi quando avreste potuto gestire i loro risultati.


3
@Tino Dipende da come definisci occupato-attesa. Vedi Qual è la differenza tra attesa occupata e polling?
Piotr Dobrogost

1
C'è un modo per interrogare un insieme di processi non solo uno?
Piotr Dobrogost

1
nota: potrebbe bloccarsi se un processo genera un output sufficiente. Dovresti consumare stdout contemporaneamente se usi PIPE (ci sono (troppi ma non abbastanza) avvertimenti a riguardo nei documenti del sottoprocesso).
jfs

@PiotrDobrogost: potresti usare os.waitpiddirettamente che permette di controllare se qualche processo figlio ha cambiato il suo stato.
jfs

5
utilizzare ['/usr/bin/my_cmd', '-i', path]invece di['/usr/bin/my_cmd', '-i %s' % path]
jfs

11

Quello che mi chiedo è se questo [os.system ()] è il modo corretto per realizzare una cosa del genere?

No. os.system()non è il modo corretto. Ecco perché tutti dicono di usare subprocess.

Per ulteriori informazioni, leggi http://docs.python.org/library/os.html#os.system

Il modulo sottoprocesso fornisce servizi più potenti per generare nuovi processi e recuperare i loro risultati; l'utilizzo di quel modulo è preferibile rispetto all'utilizzo di questa funzione. Usa il modulo sottoprocesso. Controllare in particolare la sezione Sostituzione di funzioni precedenti con la sezione Modulo sottoprocesso.


8

Ho avuto un buon successo con il modulo asyncproc , che gestisce bene l'output dei processi. Per esempio:

import os
from asynproc import Process
myProc = Process("myprogram.app")

while True:
    # check to see if process has ended
    poll = myProc.wait(os.WNOHANG)
    if poll is not None:
        break
    # print any new output
    out = myProc.read()
    if out != "":
        print out

è da nessuna parte su GitHub?
Nick

È una licenza gpl, quindi sono sicuro che sia presente molte volte. Eccone uno: github.com/albertz/helpers/blob/master/asyncproc.py
Noah

Ho aggiunto una sintesi con alcune modifiche per farlo funzionare con python3. (per lo più sostituisce str con byte). Vedi gist.github.com/grandemk/cbc528719e46b5a0ffbd07e3054aab83
Tic

1
Inoltre, è necessario leggere l'output ancora una volta dopo essere usciti dal ciclo o si perderà parte dell'output.
Tic

7

L'uso di pexpect con readline non bloccanti è un altro modo per farlo. Pexpect risolve i problemi di deadlock, consente di eseguire facilmente i processi in background e offre modi semplici per avere callback quando il processo emette stringhe predefinite e generalmente rende molto più semplice l'interazione con il processo.


4

Considerando "Non devo aspettare che torni", una delle soluzioni più semplici sarà questa:

subprocess.Popen( \
    [path_to_executable, arg1, arg2, ... argN],
    creationflags = subprocess.CREATE_NEW_CONSOLE,
).pid

Ma ... Da quello che ho letto questo non è "il modo corretto per realizzare una cosa del genere" a causa dei rischi per la sicurezza creati dalla subprocess.CREATE_NEW_CONSOLEbandiera.

Le cose chiave che accadono qui è l'uso di subprocess.CREATE_NEW_CONSOLEper creare una nuova console e .pid(restituisce l'ID del processo in modo che tu possa controllare il programma in seguito, se lo desideri) in modo da non aspettare che il programma finisca il suo lavoro.


3

Ho lo stesso problema cercando di connettermi a un terminale 3270 utilizzando il software di scripting s3270 in Python. Ora sto risolvendo il problema con una sottoclasse di Process che ho trovato qui:

http://code.activestate.com/recipes/440554/

Ed ecco l'esempio preso dal file:

def recv_some(p, t=.1, e=1, tr=5, stderr=0):
    if tr < 1:
        tr = 1
    x = time.time()+t
    y = []
    r = ''
    pr = p.recv
    if stderr:
        pr = p.recv_err
    while time.time() < x or r:
        r = pr()
        if r is None:
            if e:
                raise Exception(message)
            else:
                break
        elif r:
            y.append(r)
        else:
            time.sleep(max((x-time.time())/tr, 0))
    return ''.join(y)

def send_all(p, data):
    while len(data):
        sent = p.send(data)
        if sent is None:
            raise Exception(message)
        data = buffer(data, sent)

if __name__ == '__main__':
    if sys.platform == 'win32':
        shell, commands, tail = ('cmd', ('dir /w', 'echo HELLO WORLD'), '\r\n')
    else:
        shell, commands, tail = ('sh', ('ls', 'echo HELLO WORLD'), '\n')

    a = Popen(shell, stdin=PIPE, stdout=PIPE)
    print recv_some(a),
    for cmd in commands:
        send_all(a, cmd + tail)
        print recv_some(a),
    send_all(a, 'exit' + tail)
    print recv_some(a, e=0)
    a.wait()

3

La risposta accettata è molto vecchia.

Ho trovato una risposta moderna migliore qui:

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

e ha apportato alcune modifiche:

  1. farlo funzionare su Windows
  2. farlo funzionare con più comandi
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)))

È improbabile che i comandi di esempio funzionino perfettamente sul tuo sistema e non gestisce strani errori, ma questo codice dimostra un modo per eseguire più sottoprocessi usando asyncio e trasmettere l'output.


L'ho testato su cpython 3.7.4 in esecuzione su Windows e cpython 3.7.3 in esecuzione su Ubuntu WSL e Alpine Linux nativo
Terrel Shumway


1

Ci sono diverse risposte qui ma nessuna di esse ha soddisfatto i miei requisiti di seguito:

  1. Non voglio aspettare che il comando finisca o inquini il mio terminale con output di sottoprocesso.

  2. Voglio eseguire lo script bash con i reindirizzamenti.

  3. Voglio supportare il piping all'interno del mio script bash (ad esempio find ... | tar ...).

L'unica combinazione che soddisfa i requisiti di cui sopra è:

subprocess.Popen(['./my_script.sh "arg1" > "redirect/path/to"'],
                 stdout=subprocess.PIPE, 
                 stderr=subprocess.PIPE,
                 shell=True)

0

Questo è trattato dagli esempi di sottoprocesso di Python 3 in "Attendi la fine del comando in modo asincrono":

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()

Il processo inizierà a essere eseguito non appena await asyncio.create_subprocess_exec(...)sarà completato. Se non è finito entro il momento in cui chiami await proc.communicate(), aspetterà lì per darti lo stato dell'output. Se è finito, proc.communicate()tornerà immediatamente.

Il succo qui è simile alla risposta di Terrels ma penso che la risposta di Terrels sembri complicare le cose.

Vedere asyncio.create_subprocess_execper ulteriori informazioni.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.