Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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(无缓冲)。
  • stdinstdoutstderr 分别指定执行程序的标准输入、标准输出和标准错误文件句柄。有效值为 PIPE、现有文件描述符(正整数)、现有文件对象和 NonePIPE 表示应创建到子进程的新管道。使用 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() 方法更新。
  • 如果提供 startupinfocreationflags,它们将传递给底层的 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.Popen3popen3.Popen4 的工作原理基本上与 subprocess.Popen 相同,只是

  • subprocess.Popen 如果执行失败,则会引发异常
  • capturestderr 参数被 stderr 参数替换。
  • 必须指定 stdin=PIPEstdout=PIPE
  • popen2 默认关闭所有文件描述符,但必须使用 subprocess.Popen 指定 close_fds=True

未解决的问题

一些功能已被请求,但尚未实现。这包括

  • 支持管理整个子进程群
  • 支持管理“守护进程”
  • 用于终止子进程的内置方法

虽然这些是有用的功能,但预计以后可以轻松地添加这些功能。

  • 类似 expect 的功能,包括 pty 支持。

pty 支持高度依赖于平台,这是一个问题。此外,已经有一些其他模块提供了此类功能 [6]

向后兼容性

由于这是一个新模块,因此预计不会出现任何重大向后兼容性问题。模块名称“subprocess”可能会与其他以前具有相同名称的模块 [3] 冲突,但“subprocess”似乎是迄今为止建议的最佳名称。此模块的第一个名称是“popen5”,但此名称被认为过于不直观。有一段时间,该模块被称为“process”,但此名称已被 Trent Mick 的模块 [4] 使用。

此新模块试图替换的功能和模块(os.systemos.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