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

Python 增强提案

PEP 324 – 子进程 - 新的进程模块

作者:
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(),同时仍非常易于使用
    • 它不使用具有限制的标准 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 为真,则文件对象 stdout 和 stderr 将以文本文件打开,但行可能由 Unix 行尾约定 \n、Macintosh 约定 \r 或 Windows 约定 \r\n 中的任何一个终止。所有这些外部表示形式都被 Python 程序视为 \n。注意:此功能仅在 Python 以通用换行符支持构建时可用(默认)。此外,文件对象 stdout、stdin 和 stderr 的 newlines 属性不会被 communicate() 方法更新。
  • startupinfocreationflags(如果给定)将传递给底层的 CreateProcess() 函数。它们可以指定诸如主窗口外观和新进程的优先级等信息。(仅限 Windows)

此模块还定义了两个快捷函数

  • call(*args, **kwargs):
    运行带参数的命令。等待命令完成,然后返回 returncode 属性。

    参数与 Popen 构造函数相同。示例

    retcode = call(["ls", "-l"])
    

异常

在子进程中、在新程序开始执行之前引发的异常,将在父进程中重新引发。此外,异常对象将有一个名为“child_traceback”的额外属性,它是一个包含子进程视角的跟踪信息字符串。

最常见的异常是 OSError。例如,当尝试执行一个不存在的文件时,就会发生这种情况。应用程序应该为 OSError 做好准备。

如果使用无效参数调用 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

最后修改:2025-02-01 08:59:27 GMT