为了稍微扩展这里早期的答案,有一些通常被忽略的细节。
- 喜欢
subprocess.run()而不是subprocess.check_call()和朋友超过subprocess.call()超过subprocess.Popen()超过os.system()超过os.popen()
- 理解并可能使用
text=True,又名universal_newlines=True。
- 了解
shell=True 或shell=False 的含义以及它如何改变引用和shell 便利的可用性。
- 了解
sh 和 Bash 之间的区别
- 了解子进程如何与其父进程分离,并且通常无法更改父进程。
- 避免将 Python 解释器作为 Python 的子进程运行。
下面将更详细地介绍这些主题。
首选subprocess.run() 或subprocess.check_call()
subprocess.Popen() 函数是一个低级的主力,但正确使用它很棘手,你最终会复制/粘贴多行代码......这些代码已经作为一组更高级别的标准库方便地存在于标准库中用于各种目的的包装函数,下面将更详细地介绍。
这是documentation的一段话:
调用子流程的推荐方法是对它可以处理的所有用例使用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 object 表示(可能仍在运行的)进程,需要在子进程的剩余生命周期内从您的代码中进行管理。
也许应该强调的是,subprocess.Popen() 只是创建了一个进程。如果你把它留在那里,你就会有一个子进程与 Python 一起同时运行,因此是一个“后台”进程。如果它不需要输入或输出或以其他方式与您协调,它可以与您的 Python 程序并行执行有用的工作。
避免使用os.system() 和os.popen()
从永恒以来(好吧,从 Python 2.5 开始)os module documentation 包含了更喜欢 subprocess 而不是 os.system() 的建议:
subprocess 模块为生成新进程和检索其结果提供了更强大的工具;使用那个模块比使用这个函数更可取。
system() 的问题在于它显然依赖于系统并且不提供与子进程交互的方法。它只是运行,标准输出和标准错误超出了 Python 的范围。 Python 收到的唯一信息是命令的退出状态(零表示成功,尽管非零值的含义在某种程度上也取决于系统)。
PEP-324(上面已经提到过)包含更详细的理由说明为什么os.system 存在问题以及subprocess 如何尝试解决这些问题。
os.popen() 曾经更多的是strongly discouraged:
自 2.6 版起已弃用:此功能已过时。使用subprocess 模块。
但是,从 Python 3 的某个时候开始,它已被重新实现为简单地使用 subprocess,并重定向到 subprocess.Popen() 文档以获取详细信息。
了解并经常使用check=True
您还会注意到subprocess.call() 与os.system() 有许多相同的限制。在常规使用中,一般应该检查进程是否成功完成,subprocess.check_call() 和subprocess.check_output() 会执行此操作(后者也返回已完成子进程的标准输出)。同样,您通常应该将check=True 与subprocess.run() 一起使用,除非您特别需要允许子进程返回错误状态。
实际上,使用check=True 或subprocess.check_*,如果子进程返回非零退出状态,Python 将抛出CalledProcessError exception。
subprocess.run() 的一个常见错误是省略 check=True,如果子进程失败,则当下游代码失败时会感到惊讶。
另一方面,check_call() 和 check_output() 的一个常见问题是,当出现异常时,盲目使用这些功能的用户会感到惊讶,例如当grep 没有找到匹配项时。 (无论如何,你应该用原生 Python 代码替换 grep,如下所述。)
所有事情都算在内,您需要了解 shell 命令如何返回退出代码,以及在什么情况下它们会返回非零(错误)退出代码,并有意识地决定应该如何处理它。
理解并可能使用text=True aka universal_newlines=True
从 Python 3 开始,Python 内部的字符串是 Unicode 字符串。但不能保证子进程会生成 Unicode 输出或字符串。
(如果差异不是立即明显,建议阅读 Ned Batchelder 的 Pragmatic Unicode,如果不是完全强制性的,请阅读。如果您愿意,链接后面有一个 36 分钟的视频演示,但您自己阅读该页面可能需要花费时间明显减少。)
在内心深处,Python 必须获取一个bytes 缓冲区并以某种方式对其进行解释。如果它包含一团二进制数据,不应该解码成 Unicode 字符串,因为这是容易出错和引发错误的行为 - 正是让许多 Python 2 脚本百思不得其解的那种讨厌的行为, 在没有办法正确区分编码文本和二进制数据之前。
使用text=True,您告诉 Python 实际上,您希望返回系统默认编码的文本数据,并且应该尽 Python 的能力将其解码为 Python (Unicode) 字符串(通常是 UTF- 8 在任何适度更新的系统上,可能除了 Windows?)
如果这不是您要求返回的内容,Python 只会在stdout 和stderr 字符串中为您提供bytes 字符串。也许稍后您确实知道它们毕竟是文本字符串,并且您知道它们的编码。然后,您可以对其进行解码。
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,然后shell 从那里获取它。
使用shell=False,您可以绕过外壳将参数列表传递给操作系统。
当你没有shell时,你保存一个进程并去掉一个fairly substantial amount of hidden complexity, which may or may not harbor bugs or even security problems.
另一方面,当您没有 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)
除非您确切了解在什么情况下它可能会停止工作,否则“但它对我有用”的常见反驳不是有用的反驳。
重构示例
很多时候,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 'round-trip min/avg/max' in line:
print('{}: {}'.format(host, line))
这里有几点需要注意:
- 使用
shell=False,您不需要shell 需要围绕字符串进行引用。无论如何都要加上引号可能是一个错误。
- 在子进程中运行尽可能少的代码通常是有意义的。这使您可以在 Python 代码中更好地控制执行。
- 话虽如此,复杂的 shell 管道很乏味,有时难以在 Python 中重新实现。
重构后的代码还通过非常简洁的语法说明了 shell 真正为您做了多少——无论好坏。 Python 说 显式优于隐式,但 Python 代码 相当冗长,并且可以说看起来比实际情况更复杂。另一方面,它提供了许多点,您可以在其他事情的中间抓住控制权,我们可以轻松地将主机名与 shell 命令输出一起包含在内的增强功能就是一个简单的例子。 (这在 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 或$my_exported_var 这样的Shell 变量有时可以简单地替换为Python 变量。导出的 shell 变量可用作例如os.environ['SHELL'](export 的意思是使变量对子进程可用——一个对子进程不可用的变量显然对作为 shell 的子进程运行的 Python 不可用,反之亦然。@987654428 subprocess 方法的 @ 关键字参数允许您将子进程的环境定义为字典,因此这是使 Python 变量对子进程可见的一种方法)。使用shell=False,您将需要了解如何删除任何引号;例如,cd "$HOME" 等价于 os.chdir(os.environ['HOME']),目录名称不带引号。 (很多时候cd 无论如何都没有用或没有必要,许多初学者省略了变量周围的双引号并侥幸逃脱until one day ...)
- 重定向允许您从文件中读取作为标准输入,并将标准输出写入文件。
grep 'foo' <inputfile >outputfile 打开outputfile 用于写入,打开inputfile 用于读取,并将其内容作为标准输入传递给grep,然后其标准输出到达outputfile。这通常不难用原生 Python 代码替换。
- 管道是一种重定向形式。
echo foo | nl 运行两个子进程,其中echo 的标准输出是nl 的标准输入(在操作系统级别,在类Unix 系统中,这是单个文件句柄)。如果你不能用原生 Python 代码替换管道的一端或两端,也许考虑使用 shell,特别是如果管道有两个或三个以上的进程(尽管看看 pipes module in the Python standard library 或一些更现代的和多才多艺的第三方竞争对手)。
- 作业控制允许您中断作业、在后台运行它们、将它们返回到前台等。停止和继续进程的基本 Unix 信号当然也可以从 Python 获得。但是作业是 shell 中更高级别的抽象,它涉及进程组等,如果你想从 Python 做这样的事情,你必须了解这些。
- 在您了解 everything 基本上是一个字符串之前,在 shell 中引用可能会造成混淆。所以
ls -l / 等价于'ls' '-l' '/',但文字周围的引用是完全可选的。包含 shell 元字符的不带引号的字符串经过参数扩展、空白标记化和通配符扩展;双引号可防止空格标记化和通配符扩展,但允许参数扩展(变量替换、命令替换和反斜杠处理)。这在理论上很简单,但可能会让人感到困惑,尤其是当有多层解释时(例如远程 shell 命令)。
了解sh 和 Bash 之间的区别
subprocess 使用/bin/sh 运行您的shell 命令,除非您另有特别要求(当然在Windows 上除外,它使用COMSPEC 变量的值)。这意味着various Bash-only features like arrays, [[ etc 不可用。
如果您需要使用纯 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')
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']);但echo 是一个糟糕的例子,首先要在子进程中运行,当然)。
不要从 Python 运行 Python
这是一个有点可疑的建议;在某些情况下,将 Python 解释器作为 Python 脚本的子进程运行是有意义的,甚至是绝对要求。但很多时候,正确的做法是简单地将其他 Python 模块import 放入您的调用脚本并直接调用其函数。
如果其他 Python 脚本在您的控制之下,并且它不是模块,请考虑 turning it into one。 (这个答案已经太长了,这里就不细说了。)
如果您需要并行性,您可以使用 multiprocessing module. 在子进程中运行 Python 函数还有 threading 在单个进程中运行多个任务(它更轻量级并为您提供更多控制权,但在进程中的线程紧密耦合,并绑定到单个GIL。)