为了稍微扩展一下之前的答案,有许多细节通常被忽视。
- Prefer
subprocess.run()
over subprocess.check_call()
和朋友过来subprocess.call()
over subprocess.Popen()
over os.system()
over os.popen()
- 了解并可能使用
text=True
, aka universal_newlines=True
.
- 理解其含义
shell=True
or shell=False
以及它如何改变引用以及 shell 便利性的可用性。
- 了解之间的差异
sh
和猛击
- 了解子流程如何与其父流程分离,并且通常无法更改父流程。
- 避免将 Python 解释器作为 Python 的子进程运行。
下面将更详细地介绍这些主题。
Prefer subprocess.run()
or subprocess.check_call()
The subprocess.Popen()
函数是低级主力,但正确使用很棘手,您最终会复制/粘贴多行代码......这些代码已经作为一组用于各种目的的高级包装函数方便地存在于标准库中,这下面将更详细地介绍。
这是来自文档 https://docs.python.org/3/library/subprocess.html#using-the-subprocess-module:
调用子流程的推荐方法是使用run()
函数适用于它可以处理的所有用例。对于更高级的用例,底层Popen
接口可以直接使用。
不幸的是,这些包装函数的可用性因 Python 版本而异。
-
subprocess.run()
在Python 3.5中正式引入。它旨在取代以下所有内容。
-
subprocess.check_output()
在 Python 2.7 / 3.1 中引入。基本上相当于subprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
-
subprocess.check_call()
在 Python 2.5 中引入。基本上相当于subprocess.run(..., check=True)
-
subprocess.call()
最初是在Python 2.4中引入的subprocess
模块 (PEP-324 https://www.python.org/dev/peps/pep-0324/)。基本上相当于subprocess.run(...).returncode
高级 API 与subprocess.Popen()
重构和扩展subprocess.run()
比它所取代的旧遗留功能更具逻辑性和通用性。它返回一个CompletedProcess https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess对象,它具有各种方法,允许您从已完成的子流程中检索退出状态、标准输出以及一些其他结果和状态指示器。
subprocess.run()
如果您只需要运行一个程序并将控制权返回给 Python,那么这是一个可行的方法。对于更多涉及的场景(后台进程,可能与 Python 父程序进行交互式 I/O),您仍然需要使用subprocess.Popen()
并亲自负责所有管道。这需要对所有活动部件有相当复杂的了解,并且不应掉以轻心。比较简单的Popen object https://docs.python.org/3/library/subprocess.html#subprocess.Popen表示(可能仍在运行的)进程,需要在子进程的剩余生命周期中通过代码进行管理。
或许应该强调的是,仅仅subprocess.Popen()
只是创建一个过程。如果你就这样,你就会有一个与 Python 同时运行的子进程,因此是一个“后台”进程。如果它不需要进行输入或输出或以其他方式与您协调,它可以与您的 Python 程序并行执行有用的工作。
Avoid os.system()
and os.popen()
自时间永恒以来(好吧,自Python 2.5以来)os模块文档 https://docs.python.org/release/2.5.2/lib/os-process.html已包含首选的建议subprocess
over os.system()
:
The subprocess
模块提供了更强大的工具来生成新进程并检索其结果;使用该模块比使用此功能更好。
存在的问题system()
它显然是依赖于系统的,并且不提供与子流程交互的方法。它只是运行,标准输出和标准错误超出了 Python 的范围。 Python 收到的唯一信息是命令的退出状态(零表示成功,尽管非零值的含义也有些系统相关)。
PEP-324 https://www.python.org/dev/peps/pep-0324/(上面已经提到过)包含更详细的理由os.system
有问题以及如何subprocess
尝试解决这些问题。
os.popen()
曾经更多强烈劝阻 https://docs.python.org/2/library/os.html#os.popen:
自 2.6 版本起已弃用:此功能已过时。使用subprocess
module.
然而,从 Python 3 开始,它被重新实现为简单地使用subprocess
,并重定向到subprocess.Popen()
文档了解详细信息。
了解并经常使用check=True
你还会注意到subprocess.call()
有许多相同的限制os.system()
。在日常使用中,一般应该检查该过程是否成功完成,这subprocess.check_call()
and subprocess.check_output()
do(后者还返回已完成子流程的标准输出)。同样,您通常应该使用check=True
with subprocess.run()
除非您特别需要允许子进程返回错误状态。
在实践中,与check=True
or subprocess.check_*
,Python会抛出一个CalledProcessError例外 https://docs.python.org/3/library/subprocess.html#subprocess.CalledProcessError如果子进程返回非零退出状态。
一个常见的错误是subprocess.run()
是省略check=True
如果子流程失败,下游代码也会失败,您会感到惊讶。
另一方面,一个常见的问题是check_call()
and check_output()
是盲目使用这些功能的用户在引发异常时感到惊讶,例如什么时候grep
没有找到匹配项。 (你可能应该替换grep
无论如何,使用本机 Python 代码,如下所述。)
重要的是,您需要了解 shell 命令如何返回退出代码,以及在什么条件下它们将返回非零(错误)退出代码,并有意识地决定应该如何处理它。
了解并可能使用text=True
aka universal_newlines=True
从 Python 3 开始,Python 内部的字符串是 Unicode 字符串。但不能保证子进程会生成 Unicode 输出或根本字符串。
(如果差异不是很明显,Ned Batchelder 的实用的统一码 https://nedbatchelder.com/text/unipain.html推荐(如果不是完全强制)阅读。如果您愿意,链接后面有一个 36 分钟的视频演示,不过您自己阅读该页面可能会花费更少的时间。)
在内心深处,Python 必须获取一个bytes
缓冲并以某种方式解释它。如果它包含二进制数据块,那么它不应该被解码为 Unicode 字符串,因为这是容易出错和引发错误的行为 - 正是这种令人讨厌的行为,在有一种方法可以正确区分编码文本和二进制数据之前,困扰着许多 Python 2 脚本。
With text=True
,你告诉Python,事实上,你期望以系统默认编码返回文本数据,并且应该尽Python的能力将其解码为Python(Unicode)字符串(在任何中等最新的版本上通常为UTF-8)系统,也许 Windows 除外?)
如果那是not你请求返回什么,Python 就会给你bytes
中的字符串stdout
and stderr
字符串。也许在稍后的某个时刻你do知道它们毕竟是文本字符串,并且您知道它们的编码。然后,您可以对它们进行解码。
normal = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True,
text=True)
print(normal.stdout)
convoluted = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True)
# You have to know (or guess) the encoding
print(convoluted.stdout.decode('utf-8'))
Python 3.7 引入了更短、更具描述性和易于理解的别名text
对于以前被称为有点误导性的关键字参数universal_newlines
.
理解shell=True
vs shell=False
With shell=True
您将单个字符串传递给 shell,shell 从那里获取它。
With shell=False
您绕过 shell 将参数列表传递给操作系统。
当你没有 shell 时,你可以保存一个进程并删除一个进程相当大量的隐藏复杂性,可能会也可能不会存在错误,甚至安全问题。 /questions/3172470/actual-meaning-of-shell-true-in-subprocess
另一方面,当您没有 shell 时,您就没有重定向、通配符扩展、作业控制和大量其他 shell 功能。
一个常见的错误是使用shell=True
然后仍然向 Python 传递一个标记列表,反之亦然。这在某些情况下确实有效,但定义确实不明确,并且可能会以有趣的方式破坏。
# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short stackoverflow.com')
# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'stackoverflow.com'],
shell=True)
# XXX DEFINITELY AVOID THIS
pathological = subprocess.run(['dig +short stackoverflow.com'],
shell=True)
correct = subprocess.run(['dig', '+short', 'stackoverflow.com'],
# Probably don't forget these, too
check=True, text=True)
# XXX Probably better avoid shell=True
# but this is nominally correct
fixed_but_fugly = subprocess.run('dig +short stackoverflow.com',
shell=True,
# Probably don't forget these, too
check=True, text=True)
常见的反驳“但这对我有用”并不是一个有用的反驳,除非你确切地了解在什么情况下它会停止工作。
简单回顾一下,正确的用法看起来像
subprocess.run("string for 'the shell' to parse", shell=True)
# or
subprocess.run(["list", "of", "tokenized strings"]) # shell=False
如果您想避免使用 shell,但又太懒或不确定如何将字符串解析为标记列表,请注意shlex.split()
可以为你做这件事。
subprocess.run(shlex.split("no string for 'the shell' to parse")) # shell=False
# equivalent to
# subprocess.run(["no", "string", "for", "the shell", "to", "parse"])
常规的split()
在这里不起作用,因为它不保留引用。在上面的示例中,请注意如何"the shell"
是单个字符串。
重构示例
很多时候,shell 的功能可以用本机 Python 代码替换。简单的 awk 或sed
脚本可能应该直接翻译为 Python。
为了部分说明这一点,这里有一个典型但有点愚蠢的示例,其中涉及许多 shell 功能。
cmd = '''while read -r x;
do ping -c 3 "$x" | grep 'min/avg/max'
done <hosts.txt'''
# Trivial but horrible
results = subprocess.run(
cmd, shell=True, universal_newlines=True, check=True)
print(results.stdout)
# Reimplement with shell=False
with open('hosts.txt') as hosts:
for host in hosts:
host = host.rstrip('\n') # drop newline
ping = subprocess.run(
['ping', '-c', '3', host],
text=True,
stdout=subprocess.PIPE,
check=True)
for line in ping.stdout.split('\n'):
if 'min/avg/max' in line:
print('{}: {}'.format(host, line))
这里需要注意一些事项:
- With
shell=False
你不需要 shell 需要的字符串周围的引号。无论如何加引号可能是一个错误。
- 在子进程中运行尽可能少的代码通常是有意义的。这使您可以更好地控制 Python 代码中的执行。
- 话虽如此,在 Python 中重新实现复杂的 shell 管道非常乏味,有时甚至具有挑战性。
重构的代码还通过非常简洁的语法说明了 shell 真正为您做了多少事情——无论好坏。蟒蛇说显式优于隐式但是Python代码is相当冗长,并且可以说看起来比实际情况更复杂。另一方面,它提供了许多可以让您在其他事情中间获取控制权的点,例如我们可以轻松地将主机名与 shell 命令输出一起包含在内的增强功能。 (这在 shell 中也绝不具有挑战性,但代价是又一次转移,也许还有另一个进程。)
常见的外壳结构
为了完整起见,这里简要解释了其中一些 shell 功能,以及一些关于如何用本机 Python 工具替换它们的注释。
- 通配符扩展又名通配符扩展可以替换为
glob.glob()
或者经常使用简单的 Python 字符串比较,例如for file in os.listdir('.'): if not file.endswith('.png'): continue
。 Bash 还有各种其他扩展工具,例如.{png,jpg}
支撑扩张和{1..100}
以及波形符扩展(~
扩展到您的主目录,更一般地说~account
到另一个用户的主目录)
- 外壳变量如
$SHELL
or $my_exported_var
有时可以简单地用 Python 变量替换。导出的 shell 变量可用,例如os.environ['SHELL']
(的含义export
是使变量可用于子进程——子进程不可使用的变量显然不可用于作为 shell 子进程运行的 Python,反之亦然。这env=
关键字参数subprocess
方法允许您将子进程的环境定义为字典,因此这是使 Python 变量对子进程可见的一种方法。和shell=False
您需要了解如何删除任何引号;例如,cd "$HOME"
相当于os.chdir(os.environ['HOME'])
目录名称周围不带引号。 (常常cd
无论如何,没有用处或没有必要,许多初学者省略了变量周围的双引号并侥幸逃脱直到有一天 ... /questions/10067266/when-to-wrap-quotes-around-a-shell-variable)
- 重定向允许您从文件中读取作为标准输入,并将标准输出写入文件。
grep 'foo' <inputfile >outputfile
opens outputfile
用于写作和inputfile
用于读取,并将其内容作为标准输入传递给grep
,其标准输出然后落在outputfile
。这通常并不难用本机 Python 代码替换。
- 管道是重定向的一种形式。
echo foo | nl
运行两个子进程,其中标准输出echo
是标准输入nl
(在操作系统级别,在类 Unix 系统中,这是单个文件句柄)。如果您无法用本机 Python 代码替换管道的一端或两端,也许可以考虑使用 shell,特别是当管道有两个或三个以上进程时(尽管看看pipesPython 标准库中的模块 https://docs.python.org/3.7/library/pipes.html或一些更现代、更通用的第三方竞争对手)。
- 作业控制允许您中断作业、在后台运行它们、将它们返回到前台等。停止和继续进程的基本 Unix 信号当然也可以从 Python 获得。但作业是 shell 中的更高级别的抽象,它涉及进程组等,如果你想从 Python 中执行类似的操作,则必须了解这些内容。
- 在 shell 中引用可能会令人困惑,直到您理解这一点一切基本上是一个字符串。所以
ls -l /
相当于'ls' '-l' '/'
但对文字的引用是完全可选的。包含 shell 元字符的不带引号的字符串会经历参数扩展、空格标记化和通配符扩展;双引号可防止空格标记化和通配符扩展,但允许参数扩展(变量替换、命令替换和反斜杠处理)。这在理论上很简单,但可能会令人困惑,特别是当存在多层解释时(例如远程 shell 命令)。
了解之间的差异sh
和猛击
subprocess
运行你的 shell 命令/bin/sh
除非您另有特别要求(当然在 Windows 上除外,它使用COMSPEC
多变的)。这意味着各种仅 Bash 的功能,例如数组,[[ etc /a/42666651/874188不可用。
如果您需要使用仅限 Bash 的语法,您可以
将路径传递给 shell 作为executable='/bin/bash'
(当然,如果你的 Bash 安装在其他地方,你需要调整路径)。
subprocess.run('''
# This for loop syntax is Bash only
for((i=1;i<=$#;i++)); do
# Arrays are Bash-only
array[i]+=123
done''',
shell=True, check=True,
executable='/bin/bash')
A subprocess
与其父级分离,并且无法更改它
一个常见的错误是做类似的事情
subprocess.run('cd /tmp', shell=True)
subprocess.run('pwd', shell=True) # Oops, doesn't print /tmp
如果第一个子进程尝试设置环境变量,也会发生同样的事情,当您运行另一个子进程等时,该环境变量当然会消失。
子进程完全独立于 Python 运行,当它完成时,Python 不知道它做了什么(除了它可以从子进程的退出状态和输出推断出的模糊指示符之外)。孩子通常无法改变父母的环境;它无法设置变量、更改工作目录,或者,换句话说,在没有父级合作的情况下,无法与其父级进行通信。
在这种特殊情况下,立即解决的方法是在单个子进程中运行这两个命令;
subprocess.run('cd /tmp; pwd', shell=True)
尽管显然这个特定的用例不是很有用;相反,使用cwd
关键字参数,或者简单地os.chdir()
在运行子进程之前。类似地,为了设置变量,您可以通过以下方式操纵当前进程(及其子进程)的环境
os.environ['foo'] = 'bar'
或将环境设置传递给子进程
subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})
(更不用说明显的重构subprocess.run(['echo', 'bar'])
; but echo
当然,这首先是在子进程中运行的一个糟糕的例子)。
不要从 Python 运行 Python
这是一个有点可疑的建议;当然,在某些情况下,将 Python 解释器作为 Python 脚本的子进程运行确实有意义,甚至是绝对要求。但很多时候,正确的方法就是import
将另一个 Python 模块添加到您的调用脚本中并直接调用其函数。
如果其他 Python 脚本在您的控制之下,并且它不是模块,请考虑将其化为一 /questions/15746675/how-to-write-a-python-module-package。 (这个答案已经太长了,所以我不会在这里深入探讨细节。)
如果需要并行性,可以使用以下命令在子进程中运行 Python 函数multiprocessing module. https://docs.python.org/3/library/multiprocessing.html?highlight=multiprocessing还有threading https://docs.python.org/3/library/threading.html#module-threading它在单个进程中运行多个任务(它更轻量级,为您提供更多控制,但也更受限制,因为进程中的线程紧密耦合,并绑定到单个进程)GIL https://docs.python.org/3/glossary.html#term-global-interpreter-lock.)