subprocess — Generar procesos adicionales

Propósito:Iniciar y comunicarse con procesos adicionales.

El módulo subprocess soporta tres interfaces de programación para trabajar con procesos. La función run(), agregada en Python 3.5, es una interfaz de programación de alto nivel para ejecutar un proceso y, opcionalmente, recopilar su salida. Las funciones call(), check_call(), y check_output() son la antigua interfaz de programación de alto nivel, transferida desde Python 2. Todavía son compatibles y ampliamente utilizados en los programas existentes. La clase Popen es una interfaz de programación de bajo nivel utilizada para construir las otras interfaces de programación y es útil para interacciones de procesos más complejas. El constructor de Popen toma argumentos para configurar el nuevo proceso para que el padre pueda comunicarse con él a través de pipes. Proporciona toda la funcionalidad de los otros módulos y funciones que reemplaza, y más. La interfaz de programación es consistente para todos los usos, y muchos de los pasos adicionales de gastos generales necesarios (como cerrar un archivo extra) los descriptores y asegurar que los pipes están cerradas) están «incorporados» en lugar de ser manejados por el código de la aplicación por separado.

El módulo subprocess está destinado a reemplazar funciones como os.system(), os.spawnv(), las variaciones de popen() en los módulos os y popen2, así como el módulo comands(). Para que sea más fácil de comparar subprocess con esos otros módulos, muchos de los ejemplos en esta sección vuelve a crear las que se utilizan para os y popen2.

Nota

La interfaz de programación para trabajar en Unix y Windows es aproximadamente la misma, pero la implementación subyacente es diferente debido a la diferencia en los modelos de proceso en los sistemas operativos. Todos los ejemplos que se muestran aquí se probaron en Mac OS X. El comportamiento en un sistema operativo no-Unix puede variar.

Ejecutar un comando externo

Para ejecutar un comando externo sin interactuar con él de la misma manera que con os.system(), usa la función run().

subprocess_os_system.py
import subprocess

completed = subprocess.run(['ls', '-1'])
print('returncode:', completed.returncode)

Los argumentos de la línea de comando se pasan como una lista de cadenas, que evita la necesidad de comillas u otros caracteres especiales que se escapan que podrían ser interpretados por el shell. run() devuelve una instancia de CompletedProcess, con información sobre el proceso como el código de salida y la salida.

$ python3 subprocess_os_system.py

index.rst
interaction.py
repeater.py
signal_child.py
signal_parent.py
subprocess_check_output_error_trap_output.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_run_check.py
subprocess_run_output.py
subprocess_run_output_error.py
subprocess_run_output_error_suppress.py
subprocess_run_output_error_trap.py
subprocess_shell_variables.py
subprocess_signal_parent_shell.py
subprocess_signal_setpgrp.py
returncode: 0

Establecer el argumento shell en un valor verdadero causa que subprocess genere un proceso de shell intermedio que luego ejecuta el comando. El valor predeterminado es ejecutar el comando directamente.

subprocess_shell_variables.py
import subprocess

completed = subprocess.run('echo $HOME', shell=True)
print('returncode:', completed.returncode)

Usar un shell intermedio significa que las variables, los patrones globales y otras características especiales de shell se procesan en la cadena de comando antes de ejecutar el comando.

$ python3 subprocess_shell_variables.py

/Users/dhellmann
returncode: 0

Nota

Usar run() sin pasar check=True es equivalente a usar call(), que solo devolvió el código de salida del proceso.

Manejo de errores

El atributo returncode de CompletedProcess es el código de salida del programa. Quien ejecuta es responsable de interpretarlo para detectar errores. Si el argumento check para run() es True, el código de salida se comprueba y si indica que un error sucedió entonces se genera una excepción CalledProcessError.

subprocess_run_check.py
import subprocess

try:
    subprocess.run(['false'], check=True)
except subprocess.CalledProcessError as err:
    print('ERROR:', err)

El comando false siempre termina con un código de estado distinto de cero, que run() interpreta como un error.

$ python3 subprocess_run_check.py

ERROR: Command '['false']' returned non-zero exit status 1

Nota

Passing check=True to run() makes it equivalent to using check_call().

Captura de la salida

Los canales de entrada y salida estándar para el proceso iniciado por run() se vinculan a la entrada y salida del padre. Eso significa que el programa que llama no puede capturar la salida del comando. Pasa PIPE para los argumentos stdout y stderr para capturar la salida para su posterior procesamiento.

subprocess_run_output.py
import subprocess

completed = subprocess.run(
    ['ls', '-1'],
    stdout=subprocess.PIPE,
)
print('returncode:', completed.returncode)
print('Have {} bytes in stdout:\n{}'.format(
    len(completed.stdout),
    completed.stdout.decode('utf-8'))
)

El comando ls -1 se ejecuta con éxito, por lo que el texto que se imprime en la salida estándar es capturado y devuelto.

$ python3 subprocess_run_output.py

returncode: 0
Have 522 bytes in stdout:
index.rst
interaction.py
repeater.py
signal_child.py
signal_parent.py
subprocess_check_output_error_trap_output.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_run_check.py
subprocess_run_output.py
subprocess_run_output_error.py
subprocess_run_output_error_suppress.py
subprocess_run_output_error_trap.py
subprocess_shell_variables.py
subprocess_signal_parent_shell.py
subprocess_signal_setpgrp.py

Nota

Pasar check=True y establecer stdout en PIPE es equivalente a usar check_output().

El siguiente ejemplo ejecuta una serie de comandos en un sub-shell. Los mensajes son enviados a salida estándar y el error estándar antes de que terminen los comandos con un código de error.

subprocess_run_output_error.py
import subprocess

try:
    completed = subprocess.run(
        'echo to stdout; echo to stderr 1>&2; exit 1',
        check=True,
        shell=True,
        stdout=subprocess.PIPE,
    )
except subprocess.CalledProcessError as err:
    print('ERROR:', err)
else:
    print('returncode:', completed.returncode)
    print('Have {} bytes in stdout: {!r}'.format(
        len(completed.stdout),
        completed.stdout.decode('utf-8'))
    )

El mensaje al error estándar se imprime en la consola, pero el mensaje a la salida estándar está oculto.

$ python3 subprocess_run_output_error.py

to stderr
ERROR: Command 'echo to stdout; echo to stderr 1>&2; exit 1'
returned non-zero exit status 1

Para evitar que los mensajes de error de los comandos se ejecuten a través run() desde que se escribe en la consola, configura el parámetro stderr en la constante PIPE.

subprocess_run_output_error_trap.py
import subprocess

try:
    completed = subprocess.run(
        'echo to stdout; echo to stderr 1>&2; exit 1',
        shell=True,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )
except subprocess.CalledProcessError as err:
    print('ERROR:', err)
else:
    print('returncode:', completed.returncode)
    print('Have {} bytes in stdout: {!r}'.format(
        len(completed.stdout),
        completed.stdout.decode('utf-8'))
    )
    print('Have {} bytes in stderr: {!r}'.format(
        len(completed.stderr),
        completed.stderr.decode('utf-8'))
    )

Este ejemplo no establece check=True por lo que la salida del comando es capturada e impresa.

$ python3 subprocess_run_output_error_trap.py

returncode: 1
Have 10 bytes in stdout: 'to stdout\n'
Have 10 bytes in stderr: 'to stderr\n'

Para capturar mensajes de error al usar check_output(), establezca stderr en STDOUT, y los mensajes se fusionarán con el resto de la salida del comando.

subprocess_check_output_error_trap_output.py
import subprocess

try:
    output = subprocess.check_output(
        'echo to stdout; echo to stderr 1>&2',
        shell=True,
        stderr=subprocess.STDOUT,
    )
except subprocess.CalledProcessError as err:
    print('ERROR:', err)
else:
    print('Have {} bytes in output: {!r}'.format(
        len(output),
        output.decode('utf-8'))
    )

El orden de salida puede variar, dependiendo de cómo se aplique el búfer al flujo de salida estándar y la cantidad de datos que se imprimen.

$ python3 subprocess_check_output_error_trap_output.py

Have 20 bytes in output: 'to stdout\nto stderr\n'

Suprimir la Salida

Para los casos en los que no se debe mostrar o capturar la salida, utiliza DEVNULL para suprimir un flujo de salida. Este ejemplo suprime tanto la salida estándar como los flujos de error.

subprocess_run_output_error_suppress.py
import subprocess

try:
    completed = subprocess.run(
        'echo to stdout; echo to stderr 1>&2; exit 1',
        shell=True,
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
except subprocess.CalledProcessError as err:
    print('ERROR:', err)
else:
    print('returncode:', completed.returncode)
    print('stdout is {!r}'.format(completed.stdout))
    print('stderr is {!r}'.format(completed.stderr))

El nombre DEVNULL proviene del archivo de dispositivo especial de Unix, /dev/null, que responde con final de archivo cuando se abre para leer y recibe pero ignora cualquier cantidad de entrada al escribir.

$ python3 subprocess_run_output_error_suppress.py

returncode: 1
stdout is None
stderr is None

Trabajar con pipes directamente

Las funciones run(), call(), check_call(), y check_output() son envoltorios alrededor de la clase Popen. Usar Popen directamente da más control sobre cómo el comando se ejecuta, y cómo se procesan sus flujos de entrada y salida. Por ejemplo, al pasar diferentes argumentos para stdin, stdout y stderr es posible imitar las variaciones de os.popen().

Comunicación unidireccional con un proceso

Para ejecutar un proceso y leer todos sus resultados, establece el valor stdout en PIPE y llama a communicate().

subprocess_popen_read.py
import subprocess

print('read:')
proc = subprocess.Popen(
    ['echo', '"to stdout"'],
    stdout=subprocess.PIPE,
)
stdout_value = proc.communicate()[0].decode('utf-8')
print('stdout:', repr(stdout_value))

Esto es similar a la forma en que popen() funciona, excepto que la lectura es gestionada internamente por la instancia Popen.

$ python3 subprocess_popen_read.py

read:
stdout: '"to stdout"\n'

Para configurar una pipe para permitir que el programa de llamada escriba datos en él, establezca stdin en PIPE.

subprocess_popen_write.py
import subprocess

print('write:')
proc = subprocess.Popen(
    ['cat', '-'],
    stdin=subprocess.PIPE,
)
proc.communicate('stdin: to stdin\n'.encode('utf-8'))

Para enviar datos al canal de entrada estándar del proceso una vez, pasa los datos a communicate(). Esto es similar a usar popen() con el modo 'w'.

$ python3 -u subprocess_popen_write.py

write:
stdin: to stdin

Comunicación bidireccional con un proceso

Para configurar la instancia Popen para leer y escribir al mismo tiempo, utiliza una combinación de las técnicas anteriores.

subprocess_popen2.py
import subprocess

print('popen2:')

proc = subprocess.Popen(
    ['cat', '-'],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
)
msg = 'through stdin to stdout'.encode('utf-8')
stdout_value = proc.communicate(msg)[0].decode('utf-8')
print('pass through:', repr(stdout_value))

Esto configura el pipe para imitar a popen2().

$ python3 -u subprocess_popen2.py

popen2:
pass through: 'through stdin to stdout'

Captura de salida de error

También es posible observar las dos secuencias para stdout y stderr, como con popen3().

subprocess_popen3.py
import subprocess

print('popen3:')
proc = subprocess.Popen(
    'cat -; echo "to stderr" 1>&2',
    shell=True,
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
)
msg = 'through stdin to stdout'.encode('utf-8')
stdout_value, stderr_value = proc.communicate(msg)
print('pass through:', repr(stdout_value.decode('utf-8')))
print('stderr      :', repr(stderr_value.decode('utf-8')))

Leer desde stderr funciona igual que con stdout. Pasar PIPE le dice a Popen que se conecte al canal, y communicate() lee todos los datos antes de retornar.

$ python3 -u subprocess_popen3.py

popen3:
pass through: 'through stdin to stdout'
stderr      : 'to stderr\n'

Combinar salida regular y de error

Para dirigir la salida de error del proceso a su canal de salida estándar, usa STDOUT para stderr en lugar de PIPE.

subprocess_popen4.py
import subprocess

print('popen4:')
proc = subprocess.Popen(
    'cat -; echo "to stderr" 1>&2',
    shell=True,
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
)
msg = 'through stdin to stdout\n'.encode('utf-8')
stdout_value, stderr_value = proc.communicate(msg)
print('combined output:', repr(stdout_value.decode('utf-8')))
print('stderr value   :', repr(stderr_value))

Combinar la salida de esta manera es similar a como popen4() trabaja.

$ python3 -u subprocess_popen4.py

popen4:
combined output: 'through stdin to stdout\nto stderr\n'
stderr value   : None

Conectar segmentos de un pipe

Se pueden conectar varios comandos en una tubería, similar a como funciona el shell de Unix, creando instancias separadas de Popen y encadenando sus entradas y salidas. El atributo stdout de una instancia de Popen se usa como argumento stdin para el siguiente en la tubería, en lugar de la constante PIPE. La salida se lee desde el identificador stdout para el comando final en la tubería.

subprocess_pipes.py
import subprocess

cat = subprocess.Popen(
    ['cat', 'index.rst'],
    stdout=subprocess.PIPE,
)

grep = subprocess.Popen(
    ['grep', '.. literalinclude::'],
    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('Included files:')
for line in end_of_pipe:
    print(line.decode('utf-8').strip())

El ejemplo reproduce la línea de comando:

$ cat index.rst | grep ".. literalinclude" | cut -f 3 -d:

La tubería lee el archivo de origen reStructuredText para esta sección y encuentra todas las líneas que incluyen otros archivos, luego imprime los nombres de los archivos que se incluyen.

$ python3 -u subprocess_pipes.py

Included files:
subprocess_os_system.py
subprocess_shell_variables.py
subprocess_run_check.py
subprocess_run_output.py
subprocess_run_output_error.py
subprocess_run_output_error_trap.py
subprocess_check_output_error_trap_output.py
subprocess_run_output_error_suppress.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_setpgrp.py

Interactuar con otro comando

Todos los ejemplos anteriores suponen una cantidad limitada de interacción. El método communicate() lee toda la salida y espera que el proceso hijo termine antes de regresar. También es posible escribir a y leer desde los gestores de conductos individuales utilizados por la instancia Popen incrementalmente, a medida que el programa se ejecuta. Un programa de eco simple que lee desde la entrada estándar y escribe en la salida estándar ilustra esta técnica.

La secuencia de comandos repeater.py se usa como el proceso hijo en el siguiente ejemplo. Lee de stdin y escribe los valores en stdout, una línea a la vez hasta que no haya más entrada. También escribe un mensaje a stderr cuando comienza y se detiene, mostrando la vida útil de el proceso del niño.

repeater.py
import sys

sys.stderr.write('repeater.py: starting\n')
sys.stderr.flush()

while True:
    next_line = sys.stdin.readline()
    sys.stderr.flush()
    if not next_line:
        break
    sys.stdout.write(next_line)
    sys.stdout.flush()

sys.stderr.write('repeater.py: exiting\n')
sys.stderr.flush()

El siguiente ejemplo de interacción usa los gestores de archivos stdin y stdout propiedad de la instancia Popen en diferentes formas. En el primer ejemplo, una secuencia de cinco números se escribe a stdin del proceso, y después de que cada escritura la siguiente línea de la salida, es leída lee de nuevo. En el segundo ejemplo, los mismos cinco números son escritos pero la salida se lee de una vez usando communicate().

interaction.py
import io
import subprocess

print('One line at a time:')
proc = subprocess.Popen(
    'python3 repeater.py',
    shell=True,
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
)
stdin = io.TextIOWrapper(
    proc.stdin,
    encoding='utf-8',
    line_buffering=True,  # send data on newline
)
stdout = io.TextIOWrapper(
    proc.stdout,
    encoding='utf-8',
)
for i in range(5):
    line = '{}\n'.format(i)
    stdin.write(line)
    output = stdout.readline()
    print(output.rstrip())
remainder = proc.communicate()[0].decode('utf-8')
print(remainder)

print()
print('All output at once:')
proc = subprocess.Popen(
    'python3 repeater.py',
    shell=True,
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
)
stdin = io.TextIOWrapper(
    proc.stdin,
    encoding='utf-8',
)
for i in range(5):
    line = '{}\n'.format(i)
    stdin.write(line)
stdin.flush()

output = proc.communicate()[0].decode('utf-8')
print(output)

Las líneas "repeater.py: exit" vienen en diferentes puntos de la salida para cada estilo de bucle.

$ python3 -u interaction.py

One line at a time:
repeater.py: starting
0
1
2
3
4
repeater.py: exiting

All output at once:
repeater.py: starting
repeater.py: exiting
0
1
2
3
4

Señalización entre procesos

Los ejemplos de gestión de procesos para el módulo os incluyen una demostración de señalización entre procesos usando os.fork() y os.kill(). Dado que cada instancia Popen proporciona un atributo pid con el ID de proceso del proceso hijo, es posible hacer algo similar con subprocess. El siguiente ejemplo combina dos secuencias de comandos. Este proceso hijo configura un manejador de señal para la señal USR.

signal_child.py
import os
import signal
import time
import sys

pid = os.getpid()
received = False


def signal_usr1(signum, frame):
    "Callback invoked when a signal is received"
    global received
    received = True
    print('CHILD {:>6}: Received USR1'.format(pid))
    sys.stdout.flush()


print('CHILD {:>6}: Setting up signal handler'.format(pid))
sys.stdout.flush()
signal.signal(signal.SIGUSR1, signal_usr1)
print('CHILD {:>6}: Pausing to wait for signal'.format(pid))
sys.stdout.flush()
time.sleep(3)

if not received:
    print('CHILD {:>6}: Never received signal'.format(pid))

Esta secuencia de comandos se ejecuta como el proceso principal. Inicia signal_child.py, luego envía la señal USR1.

signal_parent.py
import os
import signal
import subprocess
import time
import sys

proc = subprocess.Popen(['python3', 'signal_child.py'])
print('PARENT      : Pausing before sending signal...')
sys.stdout.flush()
time.sleep(1)
print('PARENT      : Signaling child')
sys.stdout.flush()
os.kill(proc.pid, signal.SIGUSR1)

La salida es:

$ python3 signal_parent.py

PARENT      : Pausing before sending signal...
CHILD  26976: Setting up signal handler
CHILD  26976: Pausing to wait for signal
PARENT      : Signaling child
CHILD  26976: Received USR1

Grupos de procesos / sesiones

Si el proceso creado por Popen genera subprocesos, esos niños no recibirán ninguna señal enviada a los padres. Eso significa que cuando se usa el argumento shell para Popen será difícil hacer que el comando iniciado en el shell termine enviando SIGINT o SIGTERM.

subprocess_signal_parent_shell.py
import os
import signal
import subprocess
import tempfile
import time
import sys

script = '''#!/bin/sh
echo "Shell script in process $$"
set -x
python3 signal_child.py
'''
script_file = tempfile.NamedTemporaryFile('wt')
script_file.write(script)
script_file.flush()

proc = subprocess.Popen(['sh', script_file.name])
print('PARENT      : Pausing before signaling {}...'.format(
    proc.pid))
sys.stdout.flush()
time.sleep(1)
print('PARENT      : Signaling child {}'.format(proc.pid))
sys.stdout.flush()
os.kill(proc.pid, signal.SIGUSR1)
time.sleep(3)

El pid utilizado para enviar la señal no coincide con el pid del hijo de la secuencia de comandos de shell esperando la señal, porque en este ejemplo hay tres procesos separados que interactúan:

  1. El programa subprocess_signal_parent_shell.py
  2. El proceso de shell ejecutando la secuencia de comandos creada por el programa de python principal
  3. El programa signal_child.py
$ python3 subprocess_signal_parent_shell.py

PARENT      : Pausing before signaling 26984...
Shell script in process 26984
+ python3 signal_child.py
CHILD  26985: Setting up signal handler
CHILD  26985: Pausing to wait for signal
PARENT      : Signaling child 26984
CHILD  26985: Never received signal

Para enviar señales a los descendientes sin conocer su identificador de proceso, usa un grupo de procesos para asociar a los niños para que puedan ser señalizados juntos. El grupo de procesos se crea con os.setpgrp(), que establece el ID del grupo de procesos en el ID de proceso del proceso actual. Todos los procesos secundarios heredan el grupo de procesos de sus padres, y ya que solo debe establecerse en el shell creado por Popen y sus descendientes, os.setpgrp() no debe ser llamada en el mismo proceso donde se crea el Popen. En cambio, la función se pasa la función a Popen como el argumento preexec_fn por lo que se ejecuta después de fork() dentro del nuevo proceso, antes de que use exec() para iniciar el shell. Para dar señales a todo el grupo de procesos usa os.killpg() con el valor pid de la instancia Popen.

subprocess_signal_setpgrp.py
import os
import signal
import subprocess
import tempfile
import time
import sys


def show_setting_prgrp():
    print('Calling os.setpgrp() from {}'.format(os.getpid()))
    os.setpgrp()
    print('Process group is now {}'.format(os.getpgrp()))
    sys.stdout.flush()


script = '''#!/bin/sh
echo "Shell script in process $$"
set -x
python3 signal_child.py
'''
script_file = tempfile.NamedTemporaryFile('wt')
script_file.write(script)
script_file.flush()

proc = subprocess.Popen(
    ['sh', script_file.name],
    preexec_fn=show_setting_prgrp,
)
print('PARENT      : Pausing before signaling {}...'.format(
    proc.pid))
sys.stdout.flush()
time.sleep(1)
print('PARENT      : Signaling process group {}'.format(
    proc.pid))
sys.stdout.flush()
os.killpg(proc.pid, signal.SIGUSR1)
time.sleep(3)

La secuencia de eventos es

  1. El programa padre crea una instancia de Popen.
  2. La instancia Popen crea un nuevo proceso.
  3. El nuevo proceso ejecuta os.setpgrp().
  4. El nuevo proceso ejecuta exec () para iniciar el shell.
  5. El shell ejecuta la secuencia de comandos de shell.
  6. La secuencia de comandos de shell vuelve a bifurcarse y ese proceso ejecuta Python.
  1. Python ejecuta signal_child.py.
  1. El programa padre envía una señal al grupo de proceso utilizando el pid del shell.
  2. El shell y los procesos de Python reciben la señal.
  3. El shell ignora la señal.
  4. El proceso de Python que ejecuta signal_child.py invoca el manejador de señales.
$ python3 subprocess_signal_setpgrp.py

Calling os.setpgrp() from 75636
Process group is now 75636
PARENT      : Pausing before signaling 75636...
Shell script in process 75636
+ python3 signal_child.py
CHILD  75637: Setting up signal handler
CHILD  75637: Pausing to wait for signal
PARENT      : Signaling process group 75636
CHILD  75637: Received USR1

Ver también