PEP 324 – subprocess - 新进程模块
- 作者:
- Peter Astrand <astrand at lysator.liu.se>
- 状态:
- 最终
- 类型:
- 标准轨迹
- 创建:
- 2003年11月19日
- Python 版本:
- 2.4
- 发布历史:
摘要
本 PEP 描述了一个用于启动和与进程通信的新模块。
动机
在任何编程语言中,启动新进程都是一项常见任务,在像 Python 这样的高级语言中更是如此。需要对这项任务提供良好的支持,因为
- 不合适的进程启动函数可能意味着安全风险:如果程序是通过 shell 启动的,并且参数包含 shell 元字符,则结果可能是灾难性的。 [1]
- 它使 Python 成为更优秀的复杂 shell 脚本替代语言。
目前,Python 有大量不同的进程创建函数。这使得开发人员难以选择。
subprocess 模块提供了以下比以前函数更强大的功能
- 一个“统一”的模块提供了以前所有函数的功能。
- 跨进程异常:子进程在开始执行之前发生的异常会在父进程中重新引发。这意味着很容易处理
exec()
失败,例如。例如,使用 popen2 无法检测到执行是否失败。 - 一个在 fork 和 exec 之间执行自定义代码的钩子。这可以用于,例如,更改 uid。
- 不隐式调用 /bin/sh。这意味着无需转义危险的 shell 元字符。
- 所有文件描述符重定向组合都是可能的。例如,“python-dialog” [2] 需要生成一个进程并重定向 stderr,但不需要重定向 stdout。使用当前函数,如果不使用临时文件,这是不可能的。
- 使用 subprocess 模块,可以控制在执行新程序之前是否应该关闭所有打开的文件描述符。
- 支持连接多个子进程(shell“管道”)。
- 通用换行符支持。
- 一个
communicate()
方法,它可以轻松地发送 stdin 数据并读取 stdout 和 stderr 数据,而不会有死锁的风险。大多数人都知道子进程通信中涉及的流控制问题,但并非所有人都具备编写完全正确且无死锁的 select 循环的耐心或技能。这意味着许多 Python 应用程序包含竞争条件。标准库中的communicate()
方法解决了这个问题。
基本原理
以下几点总结了设计
- subprocess 基于 popen2,经过反复测试。
- popen2 中的工厂函数已被删除,因为我认为类构造函数同样易于使用。
- popen2 包含多个用于不同重定向组合的工厂函数和类。但是,subprocess 包含一个类。由于 subprocess 模块支持 12 种不同的重定向组合,为每种组合提供一个类或函数将很麻烦且不直观。即使使用 popen2,这也是一个可读性问题。例如,许多人无法区分 popen2.popen2 和 popen2.popen4,如果不使用文档。
- 提供了一个小的实用程序函数:
subprocess.call()
。它的目标是比os.system()
更好,同时仍然非常易于使用- 它不使用 Standard C 函数 system(),后者存在限制。
- 它不会隐式调用 shell。
- 无需引用;使用参数列表。
- 返回值更容易使用。
call()
实用程序函数接受一个“args”参数,就像Popen
类构造函数一样。它等待命令完成,然后返回returncode
属性。实现非常简单def call(*args, **kwargs): return Popen(*args, **kwargs).wait()
call()
函数背后的动机很简单:启动一个进程并等待它完成是一项常见任务。虽然
Popen
支持广泛的选项,但许多用户有简单的需求。许多人今天都在使用os.system()
,主要是因为它提供了一个简单的界面。考虑以下示例os.system("stty sane -F " + device)
使用
subprocess.call()
,这将如下所示subprocess.call(["stty", "sane", "-F", device])
或者,如果通过 shell 执行
subprocess.call("stty sane -F " + device, shell=True)
- “preexec”功能使得在 fork 和 exec 之间运行任意代码成为可能。有人可能会问,为什么有专门的参数来设置环境和当前目录,但没有参数来设置 uid,例如。答案是
- 更改环境和工作目录被认为相当常见。
- 旧函数,如
spawn()
,支持“env”参数。 - env 和 cwd 被认为是相当跨平台的:即使在 Windows 上它们也有意义。
- 在 POSIX 平台上,不需要扩展模块:该模块使用
os.fork()
、os.execvp()
等。 - 在 Windows 平台上,该模块需要 Mark Hammond 的 Windows 扩展 [5] 或一个名为 _subprocess 的小型扩展模块。
规范
此模块定义了一个名为 Popen 的类
class Popen(args, bufsize=0, executable=None,
stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=False, shell=False,
cwd=None, env=None, universal_newlines=False,
startupinfo=None, creationflags=0):
参数是
args
应为字符串或程序参数序列。要执行的程序通常是 args 序列或字符串中的第一个项目,但可以通过使用 executable 参数显式设置。在 UNIX 上,使用
shell=False
(默认值):在这种情况下,Popen
类使用os.execvp()
执行子程序。args
通常应为序列。字符串将被视为一个序列,其中字符串作为唯一项目(要执行的程序)。在 UNIX 上,使用
shell=True
:如果args
是字符串,它指定要通过 shell 执行的命令字符串。如果args
是序列,则第一个项目指定命令字符串,任何其他项目都将被视为其他 shell 参数。在 Windows 上:
Popen
类使用CreateProcess()
执行子程序,后者对字符串进行操作。如果args
是序列,它将使用list2cmdline
方法转换为字符串。请注意,并非所有 MS Windows 应用程序都以相同的方式解释命令行:list2cmdline
是为使用与 MS C 运行时相同的规则的应用程序设计的。bufsize
,如果给出,则具有与内置open()
函数相应的参数相同的含义:0 表示无缓冲,1 表示行缓冲,任何其他正值表示使用大小约为该值的缓冲区。负bufsize
表示使用系统默认值,这通常意味着完全缓冲。bufsize
的默认值为 0(无缓冲)。stdin
、stdout
和stderr
分别指定执行程序的标准输入、标准输出和标准错误文件句柄。有效值为PIPE
、现有文件描述符(正整数)、现有文件对象和None
。PIPE
表示应创建到子进程的新管道。使用None
,不会发生重定向;子进程的文件句柄将从父进程继承。此外,stderr
可以是 STDOUT,这表示应用程序的 stderr 数据应捕获到与 stdout 相同的文件句柄中。- 如果
preexec_fn
设置为可调用对象,则此对象将在子进程中执行子进程之前被调用。 - 如果
close_fds
为真,则在执行子进程之前,将关闭除 0、1 和 2 之外的所有文件描述符。 - 如果
shell
为真,则指定的命令将通过 shell 执行。 - 如果
cwd
不为None
,则在执行子进程之前,当前目录将更改为 cwd。 - 如果
env
不为None
,它定义了新进程的环境变量。 - 如果
universal_newlines
为 True,则文件对象 stdout 和 stderr 将作为文本文件打开,但行可能以\n
(Unix 行结束约定)、\r
(Macintosh 约定)或\r\n
(Windows 约定)中的任意一个字符结束。所有这些外部表示形式在 Python 程序中都被视为\n
。注意:此功能仅在 Python 使用通用换行符支持(默认设置)构建时才可用。此外,文件对象 stdout、stdin 和 stderr 的 newlines 属性不会被communicate()
方法更新。 - 如果提供
startupinfo
和creationflags
,它们将传递给底层的CreateProcess()
函数。它们可以指定主窗口的外观和新进程的优先级等内容。(仅限 Windows)
此模块还定义了两个快捷函数
call(*args, **kwargs)
:- 运行带参数的命令。等待命令完成,然后返回
returncode
属性。参数与 Popen 构造函数的参数相同。示例
retcode = call(["ls", "-l"])
异常
在子进程中,在新程序开始执行之前引发的异常将在父进程中重新引发。此外,异常对象将具有一个名为“child_traceback”的额外属性,该属性是一个包含子进程视角的回溯信息的字符串。
最常引发的异常是 OSError
。例如,当尝试执行不存在的文件时,就会发生这种情况。应用程序应做好处理 OSErrors
的准备。
如果使用无效参数调用 Popen,则会引发 ValueError
。
安全性
与其他一些 popen 函数不同,此实现永远不会隐式调用 /bin/sh。这意味着所有字符(包括 shell 元字符)都可以安全地传递给子进程。
Popen 对象
Popen 类的实例具有以下方法
poll()
- 检查子进程是否已终止。返回
returncode
属性。 wait()
- 等待子进程终止。返回
returncode
属性。 communicate(input=None)
- 与进程交互:发送数据到 stdin。读取 stdout 和 stderr 中的数据,直到到达文件结尾。等待进程终止。可选的 stdin 参数应为要发送到子进程的字符串,或者如果不需要发送任何数据到子进程,则为
None
。communicate()
返回一个元组(stdout, stderr)
。注意:读取的数据缓存在内存中,因此如果数据量很大或无限,请不要使用此方法。
以下属性也可使用
stdin
- 如果
stdin
参数为PIPE
,则此属性是提供输入到子进程的文件对象。否则,它为None
。 stdout
- 如果
stdout
参数为PIPE
,则此属性是提供子进程输出的文件对象。否则,它为None
。 stderr
- 如果
stderr
参数为PIPE
,则此属性是提供子进程错误输出的文件对象。否则,它为None
。 pid
- 子进程的进程 ID。
returncode
- 子进程的返回代码。
None
值表示进程尚未终止。负值 -N 表示子进程被信号 N 终止(仅限 UNIX)。
使用 subprocess 模块替换旧函数
在本节中,“a ==> b”表示 b 可以用作 a 的替代。
注意:如果无法找到要执行的程序,则本节中的所有函数(或多或少)都会静默失败;此模块会引发 OSError 异常。
在以下示例中,我们假设 subprocess 模块已使用 from subprocess import *
导入。
替换 /bin/sh shell 反引号
output=`mycmd myarg`
==>
output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0]
替换 shell 管道线
output=`dmesg | grep hda`
==>
p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]
替换 os.system()
sts = os.system("mycmd" + " myarg")
==>
p = Popen("mycmd" + " myarg", shell=True)
sts = os.waitpid(p.pid, 0)
注意
- 通常不需要通过 shell 调用程序。
- 查看 returncode 属性比查看退出状态更容易。
更真实的示例如下所示
try:
retcode = call("mycmd" + " myarg", shell=True)
if retcode < 0:
print >>sys.stderr, "Child was terminated by signal", -retcode
else:
print >>sys.stderr, "Child returned", retcode
except OSError, e:
print >>sys.stderr, "Execution failed:", e
替换 os.spawn*
P_NOWAIT 示例
pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
==>
pid = Popen(["/bin/mycmd", "myarg"]).pid
P_WAIT 示例
retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg")
==>
retcode = call(["/bin/mycmd", "myarg"])
向量示例
os.spawnvp(os.P_NOWAIT, path, args)
==>
Popen([path] + args[1:])
环境示例
os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env)
==>
Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"})
替换 os.popen*
pipe = os.popen(cmd, mode='r', bufsize)
==>
pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout
pipe = os.popen(cmd, mode='w', bufsize)
==>
pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin
(child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdin, child_stdout) = (p.stdin, p.stdout)
(child_stdin,
child_stdout,
child_stderr) = os.popen3(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
(child_stdin,
child_stdout,
child_stderr) = (p.stdin, p.stdout, p.stderr)
(child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
(child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout)
替换 popen2.*
注意:如果 popen2
函数的 cmd 参数是字符串,则命令将通过 /bin/sh 执行。如果是列表,则直接执行命令。
(child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode)
==>
p = Popen(["somestring"], shell=True, bufsize=bufsize
stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdout, child_stdin) = (p.stdout, p.stdin)
(child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode)
==>
p = Popen(["mycmd", "myarg"], bufsize=bufsize,
stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdout, child_stdin) = (p.stdout, p.stdin)
popen2.Popen3
和 popen3.Popen4
的工作原理基本上与 subprocess.Popen
相同,只是
subprocess.Popen
如果执行失败,则会引发异常capturestderr
参数被 stderr 参数替换。- 必须指定
stdin=PIPE
和stdout=PIPE
。 popen2
默认关闭所有文件描述符,但必须使用subprocess.Popen
指定close_fds=True
。
未解决的问题
一些功能已被请求,但尚未实现。这包括
- 支持管理整个子进程群
- 支持管理“守护进程”
- 用于终止子进程的内置方法
虽然这些是有用的功能,但预计以后可以轻松地添加这些功能。
- 类似 expect 的功能,包括 pty 支持。
pty 支持高度依赖于平台,这是一个问题。此外,已经有一些其他模块提供了此类功能 [6]。
向后兼容性
由于这是一个新模块,因此预计不会出现任何重大向后兼容性问题。模块名称“subprocess”可能会与其他以前具有相同名称的模块 [3] 冲突,但“subprocess”似乎是迄今为止建议的最佳名称。此模块的第一个名称是“popen5”,但此名称被认为过于不直观。有一段时间,该模块被称为“process”,但此名称已被 Trent Mick 的模块 [4] 使用。
此新模块试图替换的功能和模块(os.system
、os.spawn*
、os.popen*
、popen2.*
、commands.*
)预计将在未来的 Python 版本中长期可用,以保持向后兼容性。
参考实现
参考实现可从 http://www.lysator.liu.se/~astrand/popen5/ 获取。
参考文献
版权
本文档已进入公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0324.rst
上次修改: 2023-09-09 17:39:29 GMT