Per espandere un po 'le risposte precedenti qui, ci sono una serie di dettagli che sono comunemente trascurati.
- Preferisci
subprocess.run()
over subprocess.check_call()
e amici over subprocess.call()
over subprocess.Popen()
over os.system()
overos.popen()
- Comprendi e probabilmente usa
text=True
, aka universal_newlines=True
.
- Comprendi il significato
shell=True
o il shell=False
modo in cui cambia la quotazione e la disponibilità delle comodità della shell.
- Comprendi le differenze tra
sh
e Bash
- Comprendi come un sottoprocesso è separato dal suo genitore e generalmente non può cambiarlo.
- Evitare di eseguire l'interprete Python come sottoprocesso di Python.
Questi argomenti sono trattati più dettagliatamente di seguito.
Prefer subprocess.run()
osubprocess.check_call()
La subprocess.Popen()
funzione è un cavallo di lavoro di basso livello ma è difficile da usare correttamente e finisci per copiare / incollare più righe di codice ... che già esistono convenientemente nella libreria standard come un insieme di funzioni wrapper di livello superiore per vari scopi, che sono presentati in modo più dettagliato di seguito.
Ecco un paragrafo dalla documentazione :
L'approccio raccomandato per invocare i sottoprocessi è utilizzare la run()
funzione per tutti i casi d'uso che è in grado di gestire. Per casi d'uso più avanzati, l' Popen
interfaccia sottostante può essere utilizzata direttamente.
Sfortunatamente, la disponibilità di queste funzioni wrapper differisce tra le versioni di Python.
subprocess.run()
è stato ufficialmente introdotto in Python 3.5. È destinato a sostituire tutto quanto segue.
subprocess.check_output()
è stato introdotto in Python 2.7 / 3.1. È sostanzialmente equivalente asubprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
subprocess.check_call()
è stato introdotto in Python 2.5. È sostanzialmente equivalente asubprocess.run(..., check=True)
subprocess.call()
è stato introdotto in Python 2.4 nel subprocess
modulo originale ( PEP-324 ). È sostanzialmente equivalente asubprocess.run(...).returncode
API di alto livello vs subprocess.Popen()
Il refactored ed esteso subprocess.run()
è più logico e più versatile rispetto alle precedenti funzioni legacy che sostituisce. Restituisce un CompletedProcess
oggetto che ha vari metodi che consentono di recuperare lo stato di uscita, l'output standard e alcuni altri risultati e indicatori di stato dal sottoprocesso finito.
subprocess.run()
è la strada da percorrere se hai semplicemente bisogno di un programma per eseguire e restituire il controllo a Python. Per scenari più coinvolti (processi in background, forse con I / O interattivo con il programma padre Python) è comunque necessario utilizzare subprocess.Popen()
e prendersi cura di tutto l'impianto idraulico. Ciò richiede una comprensione abbastanza complessa di tutte le parti mobili e non dovrebbe essere intrapreso alla leggera. L' Popen
oggetto più semplice rappresenta il processo (possibilmente ancora in esecuzione) che deve essere gestito dal codice per il resto della durata del sottoprocesso.
Va forse sottolineato che subprocess.Popen()
crea semplicemente un processo. Se lo lasci a questo, hai un sottoprocesso in esecuzione contemporaneamente a Python, quindi un processo "in background". Se non è necessario eseguire input o output o coordinarsi in altro modo con te, può fare un utile lavoro in parallelo con il tuo programma Python.
Evita os.system()
eos.popen()
Da tempo eterno (o meglio, dal momento che Python 2.5) la os
documentazione del modulo è contenuta la raccomandazione di preferire subprocess
sopra os.system()
:
Il subprocess
modulo fornisce strutture più potenti per generare nuovi processi e recuperare i loro risultati; usare quel modulo è preferibile usare questa funzione.
Il problema system()
è che dipende ovviamente dal sistema e non offre modi per interagire con il sottoprocesso. Funziona semplicemente, con output standard ed errore standard fuori dalla portata di Python. Le uniche informazioni che Python riceve indietro sono lo stato di uscita del comando (zero significa successo, anche se il significato di valori diversi da zero dipende anche dal sistema).
PEP-324 (che è già stato menzionato sopra) contiene una logica più dettagliata del perché os.system
è problematico e di come i subprocess
tentativi di risolvere questi problemi.
os.popen()
era ancora più fortemente scoraggiato :
Obsoleto dalla versione 2.6: questa funzione è obsoleta. Usa il subprocess
modulo.
Tuttavia, da qualche tempo in Python 3, è stato reimplementato per essere semplicemente utilizzato subprocess
e reindirizza alla subprocess.Popen()
documentazione per i dettagli.
Comprendi e usa di solito check=True
Noterai anche che subprocess.call()
ha molte delle stesse limitazioni di os.system()
. In uso regolare, si dovrebbe verificare se in generale il processo terminato con successo, che subprocess.check_call()
e subprocess.check_output()
fare (se quest'ultimo restituisce anche l'uscita standard del sottoprocesso finito). Allo stesso modo, è consigliabile usarlo check=True
con a subprocess.run()
meno che non sia necessario consentire specificamente al sottoprocesso di restituire uno stato di errore.
In pratica, con check=True
o subprocess.check_*
, Python genererà CalledProcessError
un'eccezione se il sottoprocesso restituisce uno stato di uscita diverso da zero.
Un errore comune subprocess.run()
è quello di omettere check=True
ed essere sorpreso quando il codice a valle fallisce se il sottoprocesso fallisce.
D'altra parte, un problema comune con check_call()
ed check_output()
era che gli utenti che usavano ciecamente queste funzioni erano sorpresi quando veniva sollevata l'eccezione, ad esempio quando grep
non trovavano una corrispondenza. (Probabilmente dovresti sostituire comunque grep
con il codice nativo Python, come indicato di seguito.)
Tutto sommato, è necessario capire come i comandi della shell restituiscono un codice di uscita e in quali condizioni restituiranno un codice di uscita diverso da zero (errore) e prendere una decisione consapevole su come deve essere gestito esattamente.
Comprendi e probabilmente usa text=True
akauniversal_newlines=True
Da Python 3, le stringhe interne a Python sono stringhe Unicode. Ma non vi è alcuna garanzia che un sottoprocesso generi output Unicode o stringhe.
(Se le differenze non sono immediatamente evidenti, si consiglia la Pragmatic Unicode di Ned Batchelder , se non addirittura obbligatoria, la lettura. Se preferisci, c'è una presentazione video di 36 minuti dietro il link, anche se probabilmente leggere la pagina da solo richiederà molto meno tempo. )
In fondo, Python deve recuperare un bytes
buffer e interpretarlo in qualche modo. Se contiene un BLOB di dati binari, non dovrebbe essere decodificato in una stringa Unicode, perché è un comportamento soggetto a errori e che causa errori - proprio il tipo di comportamento fastidioso che ha crivellato molti script di Python 2, prima che ci fosse un modo per distinguere correttamente tra testo codificato e dati binari.
Con text=True
, dici a Python che, in effetti, ti aspetti dati testuali nella codifica predefinita del sistema e che dovrebbe essere decodificato in una stringa Python (Unicode) al meglio delle capacità di Python (di solito UTF-8 su qualsiasi moderatamente fino a sistema di data, tranne forse Windows?)
In caso contrario , Python ti fornirà solo bytes
stringhe nelle stringhe stdout
e stderr
. Forse a un certo punto si successivamente fai a sapere che erano le stringhe di testo, dopo tutto, e si conosce il loro codifica. Quindi, puoi decodificarli.
normal = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True,
text=True)
print(normal.stdout)
convoluted = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True)
# You have to know (or guess) the encoding
print(convoluted.stdout.decode('utf-8'))
Python 3.7 ha introdotto l'alias più breve, più descrittivo e comprensibile text
per l'argomento della parola chiave che in precedenza era stato chiamato in qualche modo fuorviante universal_newlines
.
Capire shell=True
vsshell=False
Con shell=True
te passa una singola stringa alla tua shell e la shell la prende da lì.
Con shell=False
te passa un elenco di argomenti al sistema operativo, bypassando la shell.
Quando non si dispone di una shell, si salva un processo e si elimina una notevole quantità di complessità nascosta, che può o meno contenere bug o persino problemi di sicurezza.
D'altra parte, quando non si dispone di una shell, non si dispone di reindirizzamento, espansione di caratteri jolly, controllo dei processi e un gran numero di altre funzionalità della shell.
Un errore comune è usare shell=True
e poi passare a Python un elenco di token, o viceversa. Ciò accade in alcuni casi, ma è davvero mal definito e potrebbe rompersi in modi interessanti.
# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short stackoverflow.com')
# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'stackoverflow.com'],
shell=True)
# XXX DEFINITELY AVOID THIS
pathological = subprocess.run(['dig +short stackoverflow.com'],
shell=True)
correct = subprocess.run(['dig', '+short', 'stackoverflow.com'],
# Probably don't forget these, too
check=True, text=True)
# XXX Probably better avoid shell=True
# but this is nominally correct
fixed_but_fugly = subprocess.run('dig +short stackoverflow.com',
shell=True,
# Probably don't forget these, too
check=True, text=True)
La replica comune "ma funziona per me" non è una confutazione utile a meno che tu non capisca esattamente in quali circostanze potrebbe smettere di funzionare.
Esempio di refactoring
Molto spesso, le funzionalità della shell possono essere sostituite con codice Python nativo. Gli Awk o gli sed
script semplici dovrebbero probabilmente essere semplicemente tradotti in Python.
Per illustrare parzialmente questo, ecco un esempio tipico ma leggermente sciocco che coinvolge molte caratteristiche della shell.
cmd = '''while read -r x;
do ping -c 3 "$x" | grep 'round-trip min/avg/max'
done <hosts.txt'''
# Trivial but horrible
results = subprocess.run(
cmd, shell=True, universal_newlines=True, check=True)
print(results.stdout)
# Reimplement with shell=False
with open('hosts.txt') as hosts:
for host in hosts:
host = host.rstrip('\n') # drop newline
ping = subprocess.run(
['ping', '-c', '3', host],
text=True,
stdout=subprocess.PIPE,
check=True)
for line in ping.stdout.split('\n'):
if 'round-trip min/avg/max' in line:
print('{}: {}'.format(host, line))
Alcune cose da notare qui:
- Con
shell=False
te non hai bisogno del preventivo che la shell richiede intorno alle stringhe. Mettere comunque le virgolette è probabilmente un errore.
- Spesso ha senso eseguire il minor codice possibile in un sottoprocesso. Questo ti dà un maggiore controllo sull'esecuzione dal tuo codice Python.
- Detto questo, le complesse pipeline di shell sono noiose e talvolta difficili da reimplementare in Python.
Il codice refactored illustra anche quanto la shell fa davvero per te con una sintassi molto concisa, nel bene e nel male. Python afferma che esplicito è meglio di implicito, ma il codice Python è piuttosto dettagliato e probabilmente sembra più complesso di quanto non sia in realtà. D'altra parte, offre una serie di punti in cui è possibile ottenere il controllo nel mezzo di qualcos'altro, come banalmente esemplificato dal miglioramento che possiamo facilmente includere il nome host insieme all'output del comando shell. (Questo non è affatto impegnativo da fare nella shell, ma a spese di un altro diversivo e forse di un altro processo.)
Costrutti Shell comuni
Per completezza, ecco alcune brevi spiegazioni di alcune di queste funzionalità della shell e alcune note su come possono essere sostituite con le funzionalità native di Python.
- L'espansione jolly Globbing aka può essere sostituita
glob.glob()
o molto spesso con semplici confronti di stringhe Python come for file in os.listdir('.'): if not file.endswith('.png'): continue
. Bash ha varie altre strutture di espansione come l' .{png,jpg}
espansione del rinforzo e {1..100}
l'espansione tilde (si ~
espande nella directory principale e più in generale ~account
nella directory principale di un altro utente)
- Le variabili shell come
$SHELL
o $my_exported_var
talvolta possono essere semplicemente sostituite con variabili Python. Variabili di shell esportati sono disponibili come ad esempio os.environ['SHELL']
(il significato export
è quello di rendere la variabile disposizione sottoprocessi -. Una variabile che non è disponibile per sottoprocessi ovviamente non essere disponibili per Python esecuzione come sottoprocesso del guscio, o viceversa La env=
parola l'argomento ai subprocess
metodi consente di definire l'ambiente del sottoprocesso come dizionario, quindi è un modo per rendere visibile una variabile Python a un sottoprocesso). Con shell=False
te dovrai capire come rimuovere eventuali preventivi; per esempio, cd "$HOME"
equivale a os.chdir(os.environ['HOME'])
senza virgolette attorno al nome della directory. (Molto spessocd
non è utile o necessario comunque, e molti principianti omettono le doppie virgolette attorno alla variabile e se ne vanno via fino a un giorno ... )
- Il reindirizzamento ti consente di leggere da un file come input standard e di scrivere l'output standard su un file.
grep 'foo' <inputfile >outputfile
si apre outputfile
per la scrittura e inputfile
per la lettura e passa i suoi contenuti come input standard a grep
, il cui output standard arriva quindi outputfile
. Questo non è generalmente difficile da sostituire con codice Python nativo.
- Le pipeline sono una forma di reindirizzamento.
echo foo | nl
esegue due sottoprocessi, in cui l'output standard di echo
è l'input standard di nl
(a livello di sistema operativo, in sistemi simili a Unix, si tratta di un handle di file singolo). Se non è possibile sostituire una o entrambe le estremità della pipeline con il codice Python nativo, forse pensare all'utilizzo di una shell dopo tutto, soprattutto se la pipeline ha più di due o tre processi (sebbene si veda il pipes
modulo nella libreria standard di Python o un numero di concorrenti di terze parti più moderni e versatili).
- Il controllo dei lavori consente di interrompere i lavori, eseguirli in background, riportarli in primo piano, ecc. I segnali Unix di base per interrompere e continuare un processo sono ovviamente disponibili anche da Python. Ma i lavori sono un'astrazione di livello superiore nella shell che coinvolge gruppi di processi ecc. Che devi capire se vuoi fare qualcosa del genere da Python.
- Citando nella shell è potenzialmente confuso fino a quando non si capisce che tutto è fondamentalmente una stringa. Quindi
ls -l /
è equivalente a 'ls' '-l' '/'
ma la citazione intorno ai letterali è completamente facoltativa. Le stringhe non quotate che contengono metacaratteri della shell subiscono espansione dei parametri, tokenizzazione degli spazi bianchi ed espansione dei caratteri jolly; le virgolette doppie impediscono la tokenizzazione degli spazi bianchi e l'espansione dei caratteri jolly, ma consentono l'espansione dei parametri (sostituzione variabile, sostituzione comando ed elaborazione barra rovesciata). Questo è semplice in teoria, ma può essere sconcertante, specialmente quando ci sono diversi livelli di interpretazione (un comando remoto della shell, per esempio).
Comprendi le differenze tra sh
e Bash
subprocess
esegue i comandi della shell a /bin/sh
meno che non sia richiesto diversamente (tranne ovviamente su Windows, dove utilizza il valore della COMSPEC
variabile). Ciò significa che non sono disponibili varie funzionalità solo Bash come array, [[
ecc .
Se è necessario utilizzare la sintassi solo Bash, è possibile passare il percorso alla shell come executable='/bin/bash'
(dove ovviamente se Bash è installato altrove, è necessario regolare il percorso).
subprocess.run('''
# This for loop syntax is Bash only
for((i=1;i<=$#;i++)); do
# Arrays are Bash-only
array[i]+=123
done''',
shell=True, check=True,
executable='/bin/bash')
A subprocess
è separato dal suo genitore e non può cambiarlo
Un errore piuttosto comune sta facendo qualcosa del genere
subprocess.run('foo=bar', shell=True)
subprocess.run('echo "$foo"', shell=True) # Doesn't work
che a parte la mancanza di eleganza tradisce anche una fondamentale mancanza di comprensione della parte "sub" del nome "sottoprocesso".
Un processo figlio viene eseguito completamente separato da Python e, al termine, Python non ha idea di ciò che ha fatto (a parte gli indicatori vaghi che può dedurre dallo stato di uscita e dall'output dal processo figlio). Un bambino generalmente non può cambiare l'ambiente del genitore; non può impostare una variabile, cambiare la directory di lavoro o, in così tante parole, comunicare con il suo genitore senza la cooperazione del genitore.
La correzione immediata in questo caso particolare consiste nell'eseguire entrambi i comandi in un singolo sottoprocesso;
subprocess.run('foo=bar; echo "$foo"', shell=True)
sebbene ovviamente questo particolare caso d'uso non richieda affatto la shell. Ricorda, puoi manipolare l'ambiente del processo corrente (e quindi anche i suoi figli) tramite
os.environ['foo'] = 'bar'
o passare un'impostazione ambientale a un processo figlio con
subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})
(per non parlare dell'ovvio refactoring subprocess.run(['echo', 'bar'])
, ma ovviamente echo
è un cattivo esempio di qualcosa da eseguire in un sottoprocesso).
Non eseguire Python da Python
Questo è un consiglio leggermente dubbio; ci sono certamente situazioni in cui ha senso o è addirittura un requisito assoluto per eseguire l'interprete Python come sottoprocesso da uno script Python. Ma molto spesso, l'approccio corretto è semplicemente import
all'altro modulo Python nello script chiamante e chiamare direttamente le sue funzioni.
Se l'altro script Python è sotto il tuo controllo e non è un modulo, considera di trasformarlo in uno . (Questa risposta è già troppo lunga, quindi non approfondirò qui i dettagli.)
Se hai bisogno di parallelismo, puoi eseguire le funzioni Python in sottoprocessi con il multiprocessing
modulo. C'è anche threading
che esegue più attività in un singolo processo (che è più leggero e ti dà più controllo, ma anche più vincolato nel fatto che i thread all'interno di un processo sono strettamente accoppiati e associati a un singolo GIL .)
cwm
. Forse hai qualche configurazione.bashrc
che imposta l'ambiente per l'uso bash interattivo?