Scopo | Genera e comunica con processi addizionali |
Versione Python | Nuovo nella 2.4 |
A partire dal 1 gennaio 2021 le versioni 2.x di Python non sono piu' supportate. Ti invito a consultare la corrispondente versione 3.x dell'articolo per il modulo subprocess
Il modulo
subprocess
fornisce un'interfaccia consistente per creare e lavorare con processi addizionali. Offre una interfaccia a livello più alto rispetto ad altri moduli disponibili, ed è destinato a rimpiazzare funzioni tipo
os.system()
,
os.spawn*()
,
os.popen*()
,
os.popen2*()
e
commands.*()
. Per facilitare il confronto di subprocess con questi altri moduli, gli esempi di seguito ricreano quelli usati per
os
e
popen
.
Il modulo
subprocess
definisce una classe:
Popen
e qualche funzione wrapper che usa quella classe. Il costruttore per
Popen
riceve parecchi parametri per facilitare l'impostazione del nuovo processo, e quindi comunicare con esso tramite le pipe. Ci si concentrerà su un codice di esempio qui; per una descrizione completa degli argomenti fare riferimento alla sezione 17.1.1. della documentazione della libreria.
Per eseguire un comando esterno senza interagire con esso, proprio come si farebbe con
os.system()
, si usa la funzione
call()
import subprocess
# Semplice comando
subprocess.call(['ls', '-1'], shell=True)
$ python subprocess_os_system.py __init__.py index.rst interaction.py repeater.py signal_child.py signal_parent.py subprocess_os_system.py subprocess_pipes.py subprocess_popen2.py subprocess_popen3.py subprocess_popen4.py subprocess_popen_read.py subprocess_popen_write.py subprocess_shell_variables.py subprocess_signal_parent_shell.py subprocess_signal_setsid.py
Quando shell è impostato a True , le variabili di shell nella stringa di comando sono espanse
import subprocess
# Comando con espansione della shell
subprocess.call('ls -1 $HOME', shell=True)
$ python subprocess_shell_variables.py %backup%~ Desktop Devel Documents DownloadedApps Downloads Dropbox Envs Library Logitech Magazines Movies Music Pictures Public Sites bender-old bin browser - logitech build cfx emacs gnupg-old.tar.gz iPod page-speed-images page-speed-javascript pip-log.txt public_html ssh_config.tar.gz texlive tmp trace.txt versioned_home_files
Passando diversi parametri per
stdin
,
stdout
, e
stderr
è possibile imitare le varianti di
os.popen()
popen
Lettura dall'output di una pipe:
import subprocess
print '\nlettura:'
proc = subprocess.Popen(['echo', '"to stdout"'],
shell=True,
stdout=subprocess.PIPE,
)
stdout_value = proc.communicate()[0]
print '\tstdout:', repr(stdout_value)
$ python -u subprocess_popen_read.py lettura: stdout: '\n'
Scrittura dell'input di una pipe:
import subprocess
print '\nscrittura:'
proc = subprocess.Popen(['cat', '-'],
shell=True,
stdin=subprocess.PIPE,
)
proc.communicate('\tstdin: to stdin\n')
$ python -u subprocess_popen_write.py scrittura: stdin: to stdin
popen2
Lettura e scrittura, come popen2:
import subprocess
print '\npopen2:'
proc = subprocess.Popen(['cat', '-'],
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
stdout_value = proc.communicate('attraverso stdin a stdout')[0]
print '\tpassa attraverso:', repr(stdout_value)
$ python -u subprocess_popen2.py popen2: passa attraverso: 'attraverso stdin a stdout'
popen3
Flussi separati per stdout ed stderr, come con popen3:
import subprocess
print '\npopen3:'
proc = subprocess.Popen('cat -; echo ";to stderr" 1>&2',
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout_value, stderr_value = proc.communicate('attraverso stdin a stdout')
print '\tpassa attraverso:', repr(stdout_value)
print '\tstderr:', repr(stderr_value)
$ python -u subprocess_popen3.py popen3: passa attraverso: 'attraverso stdin a stdout' stderr: ';to stderr\n'
popen4
stdout ed stderr sono combinati, come con popen4:
import subprocess
print '\npopen4:'
proc = subprocess.Popen('cat -; echo ";to stderr" 1>&2',
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
stdout_value, stderr_value = proc.communicate('attraverso stdin a stdout\n')
print '\tOutput combinato:', repr(stdout_value)
$ python -u subprocess_popen4.py popen4: Output combinato: 'attraverso stdin a stdout\n;to stderr\n'
Creando istanze separate di Popen e concatenando i loro input ed output, si può creare la propia conduttura di comandi proprio come in una shell Unix.
import subprocess
cat = subprocess.Popen(['cat', 'index.rst'],
stdout=subprocess.PIPE,
)
grep = subprocess.Popen(['grep', '.. include::'],
stdin=cat.stdout,
stdout=subprocess.PIPE,
)
cut = subprocess.Popen(['cut', '-f', '3', '-d:'],
stdin=grep.stdout,
stdout=subprocess.PIPE,
)
end_of_pipe = cut.stdout
print 'File inclusi:'
for line in end_of_pipe:
print '\t', line.strip()
$ python -u subprocess_pipes.py File inclusi: subprocess_os_system.py subprocess_shell_variables.py subprocess_popen_read.py subprocess_popen_write.py subprocess_popen2.py subprocess_popen3.py subprocess_popen4.py subprocess_pipes.py repeater.py interaction.py signal_child.py signal_parent.py subprocess_signal_parent_shell.py subprocess_signal_setsid.py
Tutti gli esempi sopra riportati presuppongono una limitata interazione. Il metodo
communicate()
legge tutto l'output ed attende che il processo figlio esca prima di ritornare. E' anche possibile scrivere verso e leggere da singoli gestori di pipe usati dalla istanza di
Popen
. Un semplice programma che legge dallo standard input e scrive verso lo standard output illustra questa situazione:
import sys
sys.stderr.write('repeater.py: inizio\n')
sys.stderr.flush()
while True:
next_line = sys.stdin.readline()
if not next_line:
break
sys.stdout.write(next_line)
sys.stdout.flush()
sys.stderr.write('repeater.py: uscita\n')
sys.stderr.flush()
Si prenda nota del fatto che repeater.py scrive allo standard error quando parte e si interrompe. Quella informazione può essere usata per mostrare il ciclo di vita del processo figlio.
Il successivo esempio di interazione usa i gestori di file stdin ed stdout che appartengono all'istanza di
Popen
in modi diversi. Nel primo esempio, una sequenza di 10 numeri vengono scritti allo stdin del processo, e dopo ogni scrittura la successiva riga di output viene riletta. Nel secondo esempio, gli stessi 10 numeri sono scritti ma l'output viene letto in una sola volta usando
communicate()
.
import subprocess
print 'Una riga alla volta:'
proc = subprocess.Popen('python repeater.py',
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
for i in range(10):
proc.stdin.write('%d\n' % i)
output = proc.stdout.readline()
print output.rstrip()
remainder = proc.communicate()[0]
print remainder
print
print "Tutto l'output in una volta:"
proc = subprocess.Popen('python repeater.py',
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
for i in range(10):
proc.stdin.write('%d\n' % i)
output = proc.communicate()[0]
print output
Si noti dove le righe "repeater.py: uscita" si trovano nell'output per ogni ciclo:
$ python -u interaction.py Una riga alla volta: repeater.py: inizio 0 1 2 3 4 5 6 7 8 9 repeater.py: uscita Tutto l'output in una volta: repeater.py: inizio repeater.py: uscita 0 1 2 3 4 5 6 7 8 9
Gli esempi di os comprendono una dimostrazione della segnalazione tra processi usando os.fork() ed os.kill(). VIsto che ogni istanza di Popen fornisce un attributo pid con l'identificativo del processo figlio, è possibile fare qualcosa di simile con subprocess . Ad esempio usare questo script per fare in modo che il processo figlio venga eseguito dal processo genitore:
import os
import signal
import time
import sys
pid = os.getpid()
received = False
def signal_usr1(signum, frame):
"""Callback chiamato quando viene ricevuto un segnale"""
global received
received = True
print 'FIGLIO %s: Ricevuto USR1' % pid
sys.stdout.flush()
print 'FIGLIO %s: Impostazione del gestore di segnale' % pid
sys.stdout.flush()
signal.signal(signal.SIGUSR1, signal_usr1)
print 'FIGLIO %s: In pausa in attesa del segnale' % pid
sys.stdout.flush()
time.sleep(3)
if not received:
print 'FIGLIO %s: Segnale mai ricevuto' % pid
e questo processo genitore:
import subprocess
import time
import sys
proc = subprocess.Popen(['python', 'signal_child.py'])
print 'GENITORE: In pausa prima di inviare il segnale...'
sys.stdout.flush()
time.sleep(1)
print 'GENITORE: Segnalazione al figlio'
sys.stdout.flush()
os.kill(proc.pid, signal.SIGUSR1)
Il risultato dovrebbe assomigliare a questo:
$ python -u signal_parent.py GENITORE: In pausa prima di inviare il segnale... FIGLIO 3250: Impostazione del gestore di segnale FIGLIO 3250: In pausa in attesa del segnale GENITORE: Segnalazione al figlio FIGLIO 3250: Ricevuto USR1
A causa del modo in cui funziona l'albero dei processi sotto Unix, se il processo creato da Popen genera dei sotto processi, questi suoi figli non riceveranno alcuno dei segnali inviati al genitore, Questo significa, ad esempio, che sarà difficile farli terminare inviandogli un SIGINT od un SIGTERM .
import os
import signal
import subprocess
import tempfile
import time
import sys
script = '''#!/bin/sh
echo "Shell script in esecuzione $$"
set -x
python signal_child.py
'''
script_file = tempfile.NamedTemporaryFile('wt')
script_file.write(script)
script_file.flush()
proc = subprocess.Popen(['sh %s' % script_file.name], shell=True, close_fds=True)
print 'GENITORE: In pausa prima di inviare il segnale al figlio %s...' % proc.pid
sys.stdout.flush()
time.sleep(1)
print 'GENITORE: Segnalazione al figlio %s' % proc.pid
sys.stdout.flush()
os.kill(proc.pid, signal.SIGUSR1)
time.sleep(3)
Si noti che il pid usato per inviare il segnale è diverso dal pid del figlio dello script della shell che è in attesa del segnale perchè in questo esempio, ci sono tre processi separati che interagiscono.:
$ python subprocess_signal_parent_shell.py GENITORE: In pausa prima di inviare il segnale al figlio 2592... Shell script in esecuzione 2593 + python signal_child.py FIGLIO 2594: Impostazione del gestore di segnale FIGLIO 2594: In pausa in attesa del segnale GENITORE: Segnalazione al figlio 2592 FIGLIO 2594: Segnale mai ricevuto
La soluzione di questo problema è usare un
gruppo di processi
da associare ai figli in modo che possa essere inviata una segnalazione a tutti insieme. Il gruppo di processi viene creato con
os.setsid()
, impostando l'identificativo di sessione ("session id") all'id del processo corrente. Tutti i processi figlio ereditano il "session id", e visto che lo si vuole solamente impostare nella shell creata da
Popen
ed i suoi discendenti non occorre chiamare il processo corrente, al contrario lo si passa come parametro
preexec_fn
a Popen in mdodo che possa essere eseguito dopo il
fork()
all'interno del nuovo processo, prima che chiami
exec()
.
import os
import signal
import subprocess
import tempfile
import time
import sys
script = '''#!/bin/sh
echo "Shell script in esecuzione $$"
set -x
python signal_child.py
'''
script_file = tempfile.NamedTemporaryFile('wt')
script_file.write(script)
script_file.flush()
proc = subprocess.Popen(['sh %s' % script_file.name],
shell=True,
close_fds=True,
preexec_fn=os.setsid,
)
print 'GENITORE: In pausa prima di inviare il segnale al figlio %s...' % proc.pid
sys.stdout.flush()
time.sleep(1)
print 'GENITORE: Segnalazione del gruppo di processo %s' % proc.pid
sys.stdout.flush()
os.killpg(proc.pid, signal.SIGUSR1)
time.sleep(3)
Per segnalare all'intero gruppo di processi, si usa
os.killpg()
con il valore del pid dall'istanza di
Popen
$ python subprocess_signal_setsid.py GENITORE: In pausa prima di inviare il segnale al figlio 2957... Shell script in esecuzione 2958 + python signal_child.py FIGLIO 2959: Impostazione del gestore di segnale FIGLIO 2959: In pausa in attesa del segnale GENITORE: Segnalazione del gruppo di processo 2957 FIGLIO 2959: Ricevuto USR1
Come si vede, lavorare con subprocess è molto più facile di fork, exex, e le pipe da soli. Fornisce tutte le funzionalità degli altri moduli e delle funzioni che sostituisce, ed altro. L'API è consistente per tutti gli utilizzi e la maggior parte dell'attività extra necessaria (tipo chiudere i descrittori di file extra, assicurarsi che le pipe siano chiuse, ecc) sono incorporate in esso invece che dover essere gestite separatamente dal codice della propria applicazione.
Vedere anche: