为了稍微扩展此处的早期答案,通常会忽略许多细节。
- 身高
subprocess.run()
在subprocess.check_call()
和朋友过subprocess.call()
过subprocess.Popen()
过os.system()
过os.popen()
- 理解并可能使用
text=True
,又名universal_newlines=True
。
- 了解
shell=True
or 的含义shell=False
以及它如何更改报价以及shell便利的可用性。
- 了解
sh
和Bash 之间的差异
- 了解子流程如何与其父流程分离,并且通常无法更改父流程。
- 避免将Python解释器作为Python的子进程运行。
这些主题将在下面更详细地介绍。
更喜欢subprocess.run()
还是subprocess.check_call()
的 subprocess.Popen()
函数是低级主力,但正确使用起来很棘手,最终您会复制/粘贴多行代码...这些代码已经方便地存在于标准库中,作为一组用于各种用途的高级包装函数,下面将更详细地介绍。
这是文档中的一段:
推荐的调用子流程的方法是将该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)中引入的。它基本上相当于subprocess.run(...).returncode
高级API与 subprocess.Popen()
与subprocess.run()
它替代的旧版旧功能相比,重构和扩展的逻辑更加丰富,用途更广。它返回一个CompletedProcess
具有各种方法的对象,使您可以从完成的子流程中检索退出状态,标准输出以及其他一些结果和状态指示符。
subprocess.run()
如果您只需要一个程序来运行并将控制权返回给Python,则可以采用这种方法。对于更复杂的场景(后台进程,也许通过Python父程序使用交互式I / O),您仍然需要使用subprocess.Popen()
并照顾好所有管道。这需要对所有运动部件有相当复杂的了解,因此不应掉以轻心。更简单的Popen
对象表示(可能仍在运行)的进程,在子进程的剩余生命周期中,需要从您的代码中对其进行管理。
也许应该强调,仅仅subprocess.Popen()
是创造一个过程。如果不这样做的话,您将有一个子进程与Python同时运行,因此是一个“后台”进程。如果不需要进行输入或输出或与您进行协调,则可以与Python程序并行进行有用的工作。
避免os.system()
和os.popen()
自从永恒(从Python 2.5开始)以来,os
模块文档中就包含了建议优先subprocess
于os.system()
:
该subprocess
模块提供了更强大的功能来生成新流程并检索其结果。使用该模块优于使用此功能。
问题system()
在于它显然依赖于系统,并且没有提供与子流程进行交互的方法。它运行简单,标准输出和标准错误超出了Python的范围。Python收到的唯一信息是命令的退出状态(零表示成功,尽管非零值的含义在某种程度上也取决于系统)。
PEP-324(上面已经提到过)包含了更详细的理由,说明了为什么os.system
会出现问题以及如何subprocess
尝试解决这些问题。
os.popen()
过去更不鼓励:
从2.6版开始不推荐使用:此功能已过时。使用subprocess
模块。
但是,自从Python 3发行以来,它已经重新实现为仅使用subprocess
,并重定向到subprocess.Popen()
文档以获取详细信息。
了解并通常使用 check=True
您还会注意到,它与subprocess.call()
有许多相同的限制os.system()
。在常规使用中,通常应该检查流程是否成功完成,执行subprocess.check_call()
和subprocess.check_output()
执行(其中后者还返回完成的子流程的标准输出)。同样,除非特别需要允许子流程返回错误状态check=True
,subprocess.run()
否则通常应使用with 。
实际上,使用check=True
或时subprocess.check_*
,如果子进程返回非零退出状态,Python将抛出CalledProcessError
异常。
一个常见的错误subprocess.run()
是check=True
如果子进程失败,则在下游代码失败时忽略并感到惊讶。
在另一方面,有一个共同的问题check_call()
和check_output()
是谁盲目使用这些功能的用户,当异常发生时,如感到惊讶grep
并没有找到匹配。(您可能应该grep
仍然用本机Python代码替换,如下所述。)
所有事情都计算在内,您需要了解shell命令如何返回退出代码,以及在什么条件下它们将返回非零(错误)退出代码,并做出有意识的决定,如何精确地处理它。
了解并且可能使用text=True
akauniversal_newlines=True
从Python 3开始,Python内部的字符串是Unicode字符串。但是,不能保证子进程会生成Unicode输出或字符串。
(如果差异不是立即显而易见的,则建议使用Ned Batchelder的实用Unicode(如果不是必须的话)阅读。如果您愿意,可以在链接后进行36分钟的视频演示,尽管您自己阅读页面的时间可能会大大减少。 )
深入地讲,Python必须获取bytes
缓冲区并以某种方式解释它。如果它包含二进制数据的斑点,则不应将其解码为Unicode字符串,因为这是容易出错和引起错误的行为-正是这种讨厌的行为,使许多Python 2脚本充满了麻烦,之后才有办法正确区分编码文本和二进制数据。
使用text=True
,您可以告诉Python您实际上希望以系统的默认编码返回文本数据,并且应将其解码为Python(Unicode)字符串,以达到Python的最佳能力(通常,UTF-8在不超过日期系统,也许除了Windows?)
如果您没有要求,Python只会bytes
在stdout
和stderr
字符串中提供字符串。也许稍后您确实知道它们是文本字符串,并且知道了它们的编码。然后,您可以解码它们。
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
与shell=False
随着shell=True
您将单个字符串传递给您的外壳,外壳便从那里接收它。
随着shell=False
您将参数列表传递给操作系统,绕过了外壳程序。
当没有外壳时,您可以保存进程并摆脱相当多的隐藏复杂性,这些复杂性可能会也可能不会包含错误甚至安全问题。
另一方面,当您没有外壳程序时,就没有重定向,通配符扩展,作业控制和大量其他外壳程序功能。
一个常见的错误是使用shell=True
Python,然后仍将令牌列表传递给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)
常见的反驳“但对我有用”不是一个有用的反驳,除非您确切地了解它在什么情况下会停止工作。
重构实例
通常,shell的功能可以用本地Python代码替换。简单的Awk或sed
脚本可能应该简单地翻译成Python。
为了部分说明这一点,这是一个典型但有些愚蠢的示例,其中涉及许多外壳功能。
cmd = '''while read -r x;
do ping -c 3 "$x" | grep 'round-trip 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 'round-trip min/avg/max' in line:
print('{}: {}'.format(host, line))
这里要注意一些事情:
- 随着
shell=False
你不需要引用的外壳需要大约字符串。无论如何用引号引起来可能是一个错误。
- 在子流程中运行尽可能少的代码通常是有意义的。这使您可以从Python代码中更好地控制执行。
- 话虽这么说,复杂的Shell管道非常繁琐,有时很难在Python中重新实现。
重构后的代码还以非常简洁的语法说明了shell实际为您做了多少-更好或更坏。蟒蛇说明确优于隐式,但Python代码是相当冗长,可以说是看起来复杂得多,这确实是。另一方面,它提供了许多要点,您可以在其中进行控制,例如通过增强功能可以很容易地说明这一点,我们可以轻松地将主机名与shell命令输出一起包括在内。(这在shell中也绝非挑战性,但是以另一种转移和也许另一种过程为代价的。)
普通壳结构
为了完整起见,这里简要介绍了其中一些外壳程序功能,并提供了一些注释,说明如何用本地Python设施替换它们。
- Globbing aka通配符扩展可以
glob.glob()
用python或类似的简单Python字符串比较代替,或者经常用for file in os.listdir('.'): if not file.endswith('.png'): continue
。Bash具有各种其他扩展功能,例如.{png,jpg}
大括号扩展和{1..100}
波浪号扩展(~
扩展到您的主目录,并且更广泛~account
地扩展到另一个用户的主目录)
- Shell变量(例如
$SHELL
或$my_exported_var
有时可以简单地用Python变量替换)。导出的shell变量可作为例如os.environ['SHELL']
(的意思export
是使变量提供给子进程-一个变量,它是不可用的子进程显然不会提供给Python的运行作为shell的子进程,反之亦然env=
关键字subprocess
方法的参数可让您将子流程的环境定义为字典,因此这是使Python变量对子流程可见的一种方法。与shell=False
您将需要了解如何删除任何引号;例如,cd "$HOME"
相当于os.chdir(os.environ['HOME'])
目录名周围不带引号。(常常cd
是不是有用的或必要的,无论如何,和很多新手忽略了双引号周围的变量,并摆脱它,直到有一天......)
- 重定向允许您从文件读取作为标准输入,并将标准输出写入文件。
grep 'foo' <inputfile >outputfile
打开outputfile
以进行写入和inputfile
阅读,并将其内容作为标准输入传递给grep
,然后其标准输出进入outputfile
。通常,用本机Python代码替换它并不难。
- 管道是重定向的一种形式。
echo foo | nl
运行两个子进程,其中的标准输出echo
是的标准输入nl
(在OS级别,在类Unix系统中,这是一个文件句柄)。如果您无法用本机Python代码替换管道的一端或两端,则也许可以考虑使用外壳程序,尤其是在管道具有两个或三个以上进程的情况下(尽管请查看pipes
Python标准库中的模块或多个更具现代性和多功能的第三方竞争对手)。
- 作业控制使您可以中断作业,在后台运行它们,将它们返回到前台等。当然,Python也提供了停止和继续一个进程的基本Unix信号。但是作业是外壳程序中的一个更高层次的抽象,涉及流程组等,如果您想从Python中进行类似的工作,则必须理解。
- 除非您了解所有内容基本上都是字符串,否则在外壳程序中进行报价可能会造成混淆。因此
ls -l /
等效于,'ls' '-l' '/'
但文字的引号是完全可选的。包含外壳元字符的未加引号的字符串将进行参数扩展,空格标记化和通配符扩展;双引号可防止空格标记化和通配符扩展,但允许参数扩展(变量替换,命令替换和反斜杠处理)。从理论上讲这很简单,但是可能会令人困惑,尤其是在存在多层解释时(例如,远程shell命令)。
了解sh
和Bash 之间的差异
subprocess
/bin/sh
除非另有明确要求,否则将使用shell命令运行外壳命令(当然,在Windows上除外,因为Windows使用COMSPEC
变量的值)。这意味着各种仅Bash的功能(如数组[[
等)不可用。
如果您需要使用仅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('foo=bar', shell=True)
subprocess.run('echo "$foo"', shell=True) # Doesn't work
除了缺乏优雅之外,这还背叛了人们对“ subprocess”名称中“ sub”部分的根本性了解。
子进程与Python完全独立运行,完成时,Python不知道它做了什么(除了模糊的指示,它可以从子进程的退出状态和输出中推断出来)。孩子通常不能改变父母的环境;它不能设置变量,更改工作目录,也不能在没有上级合作的情况下与其上级通信。
在这种情况下,立即解决的办法是在一个子进程中运行两个命令。
subprocess.run('foo=bar; echo "$foo"', shell=True)
尽管显然,这种特定用例根本不需要外壳。请记住,您可以通过以下方式操纵当前进程的环境(因此也可以操纵其子进程)
os.environ['foo'] = 'bar'
或通过以下方式将环境设置传递给子进程
subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})
(更不用说明显的重构了subprocess.run(['echo', 'bar'])
;但是echo
,当然,这是在子流程中首先要运行的一个糟糕的例子)。
不要从Python运行Python
这是些可疑的建议。当然,在某些情况下,将Python解释器作为Python脚本的子进程运行甚至是绝对必要的情况。但是通常,正确的方法只是import
将另一个Python模块放入您的调用脚本中,然后直接调用其功能。
如果其他Python脚本在您的控制下,并且不是模块,请考虑将其转换为一个。(此答案已经太久了,因此在这里我将不做详细介绍。)
如果需要并行处理,则可以在带有multiprocessing
模块的子进程中运行Python函数。 还有一个可以threading
在单个进程中运行多个任务(它更轻巧,可以为您提供更多控制权,但同时也更多地限制了进程中的线程紧密耦合并绑定到单个GIL。)
cwm
。也许您有一些配置.bashrc
可以设置交互式bash使用环境?