PyMOTW: subprocess

  • 模块: subprocess
  • 目的: 孵化子进程和进程间通信
  • python版本: 2.4+

描述

subprocess模块提供了用于创建进程和进程间通信的一致接口. 它提供给我们一个比其他可用模块更高层次的接口, 可以替换如os.system, os.spawn*, os.popen*, popen2.* and commands.*这些函数. 为了更简单的比较subprocess和其他模块, 我会重构之前的例子来说明.

subprocess模块定义了一个类, Popen()和一些封装函数使用这个类. Popen()指定多个参数以便于创建新进程, 并可以通过管道进行通信. 这里我会集中在一个例子上; 对于这个类的参数的详细介绍可以参见库文档的 17.1.1节 .

可移植性

在各平台上, API几乎是一样的, 但底层的实现, Unix和Windows稍有不同. 下面的所有例子是在Mac OS X上测试的. 如果你在非Unix操作系统上进行测试, 结果会有所不同.

运行外部命令

想要执行一条外部命令并且没有交互, 比如用os.system(),这里就可以用call()函数.

import subprocess

# Simple command
subprocess.call('ls -l', shell=True)
$ python replace_os_system.py
total 16
-rw-r--r-- 1 dhellman dhellman 0 Jul 1 11:29 __init__.py
-rw-r--r-- 1 dhellman dhellman 1316 Jul 1 11:32 replace_os_system.py
-rw-r--r-- 1 dhellman dhellman 1167 Jul 1 11:31 replace_os_system.py~

由于我们设置了shell=True, 所以命令字符串中的shell变量会被扩展开.

..note:

不设置shell为True, 前面的命令不能运行.
# Command with shell expansion
subprocess.call('ls -l $HOME', shell=True)
total 40
drwx------ 10 dhellman dhellman 340 Jun 30 18:45 Desktop
drwxr-xr-x 15 dhellman dhellman 510 Jun 19 07:08 Devel
drwx------ 29 dhellman dhellman 986 Jun 29 07:44 Documents
drwxr-xr-x 44 dhellman dhellman 1496 Jun 29 09:51 DownloadedApps
drwx------ 55 dhellman dhellman 1870 May 22 14:53 Library
drwx------ 8 dhellman dhellman 272 Mar 4 2006 Movies
drwx------ 11 dhellman dhellman 374 Jun 21 07:04 Music
drwx------ 12 dhellman dhellman 408 Jul 1 01:00 Pictures
drwxr-xr-x 5 dhellman dhellman 170 Oct 1 2006 Public
drwxr-xr-x 15 dhellman dhellman 510 May 12 15:19 Sites
drwxr-xr-x 5 dhellman dhellman 170 Oct 5 2005 cfx
drwxr-xr-x 4 dhellman dhellman 136 Jan 23 2006 iPod
-rw-r--r-- 1 dhellman dhellman 204 Jun 18 17:07 pgadmin.log
drwxr-xr-x 3 dhellman dhellman 102 Apr 29 16:32 tmp

读取另一条命令的输出结果

通过传递给stdin, stdout和stderr不同的参数, 可以模仿os.popen()的功能.

从管道输出中读取数据:

print '\nread:'
proc = subprocess.Popen('echo "to stdout"',
                         shell=True,
                         stdout=subprocess.PIPE,
                       )
stdout_value = proc.communicate()[0]
print '\tstdout:', repr(stdout_value)

将数据写入管道:

print '\nwrite:'
proc = subprocess.Popen('cat -',
                             shell=True,
                             stdin=subprocess.PIPE,
                           )
proc.communicate('\tstdin: to stdin\n')

读取和写入, 类似popen2:

print '\npopen2:'

proc = subprocess.Popen('cat -',
                             shell=True,
                             stdin=subprocess.PIPE,
                             stdout=subprocess.PIPE,
                           )
stdout_value = proc.communicate('through stdin to stdout')[0]
print '\tpass through:', repr(stdout_value)

区分stdout和stderr流, 类似popen3:

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('through stdin to stdout')
print '\tpass through:', repr(stdout_value)
print '\tstderr:', repr(stderr_value)

合并stdout和stderr流, 类似popen4:

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('through stdin to stdout\n')
print '\tcombined output:', repr(stdout_value)

输出如下:

read:
     stdout: 'to stdout\n'

write:
         stdin: to stdin

popen2:
     pass through: 'through stdin to stdout'

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

popen4:
             combined output: 'through stdin to stdout\n;to stderr\n'

上面所有的例子都有一个有限的交互, communicate()函数读取了所有输出, 并等待子进程退出之后返回.使用Popen实例也可从单独的管道中读取和写入数据. 下面使用这个简单的回显程序加以说明, 它读取标准输入中的数据并将数据写回标准输出.

import sys

sys.stderr.write('repeater.py: starting\n')
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: exiting\n')

注意这样一个事实, 当 repeater.py运行开始和结束时都会输出一些信息到stderr. 在下面的例子中, 我们可以用 这种方式来显示子进程的生命周期. 下面的交互式例子以不同的方式使用了Popen实例的stdin和stdout文件句柄. 在第一个例子中, 10个数字序列被写入到进程的stdin中, 每次输入的数字会在它的下一行被读取并输出. 在第二个例子中, 同样的10个数会被写入但会立即使用communicate()读取所有的输出.

import subprocess

print 'One line at a time:'
proc = subprocess.Popen('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()
proc.communicate()

print
print 'All output at once:'
proc = subprocess.Popen('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

注意到”repeater.py: exiting”这句话在每个循环输出中的出现位置:

$ python interaction.py
One line at a time:
repeater.py: starting
0
1
2
3
4
5
6
7
8
9
repeater.py: exiting

All output at once:
repeater.py: starting
repeater.py: exiting
0
1
2
3
4
5
6
7
8
9

进程间的信号

在os模块系列的第四部分,我引入了一个使用os.fork()和os.kill()进程间发送信号的例子. 每个Popen实例提供了一个pid属性以指示子进程的进程id, 使用subprocess同样可以实现类似的东西. 例如这个例子, 我为子进程设置了一个独立的脚本并在父进程中执行.

import os
import signal
import time

def signal_usr1(signum, frame):
    "Callback invoked when a signal is received"
    pid = os.getpid()
    print 'Received USR1 in process %s' % pid


print 'CHILD: Setting up signal handler'
signal.signal(signal.SIGUSR1, signal_usr1)
print 'CHILD: Pausing to wait for signal'
time.sleep(5)

父进程中:

import os
import signal
import subprocess
import time

proc = subprocess.Popen('signal_child.py')
print 'PARENT: Pausing before sending signal...'
time.sleep(1)
print 'PARENT: Signaling %s' % proc.pid
os.kill(proc.pid, signal.SIGUSR1)

输出如下:

$ python signal_parent.py
CHILD: Setting up signal handler
CHILD: Pausing to wait for signal
PARENT: Pausing before sending signal...
PARENT: Signaling 4124
Received USR1 in process 4124

结论

正如你看到的, subprocess比fork, exec和管道更加容易使用. 它提供了其他模块和函数的所有功能, 并且会更多. API的使用都保持着一致性, 许多额外的东西(如关闭时的额外文件描述符, 保证管道关闭等)被内置了而不用特地在你的代码中处理.