Pythonからシェルを実行するsubprocessモジュールのcall、check_output、run、popenの違い

Pythonからシェルを実行したい。
subprocessモジュールからcallcheck_outputrunPopenを実行することで出来るので、それぞれの違いを見ていく。
本記事はPython3.7.3で動かしている。

subprocessモジュールのまとめ

メソッド 概要
call メソッドの戻り値は終了ステータス。
check_output メソッドの戻り値は出力の内容。
終了ステータスが0以外の場合、例外が投げられるためハンドリングが必要。
run 公式のドキュメントでサブプロセスを起動する時に使うことを推奨されている。Python3.6から使える。終了ステータス、標準出力、標準エラー出力を取得できる。
Popen(メソッドではなくクラス) シェル起動後にシェルの終了を待たず後続処理を実行できる。また、communicateメソッドを使うことでシェルの終了を待ち後続処理を実行することもできる。その場合、終了ステータス、標準出力、標準エラー出力を取得できる。

結論として、シェルを実行するときはPython3.6以上であればsubprocess.runを使えば良い。 シェルの終了を待たずに後続処理を実行したい場合はsubprocess.Popenを使う。

Pythonから実行するシェルを用意する

以下のように2秒後にechoで標準出力するシェルを用意する。
wait.sh

#!/bin/sh

sleep 2
echo 'hello'

実行権限を付与してこのシェルを実行すると、sleepで指定した2秒後に標準出力にhelloと出力される。
終了ステータスは0が返される。

$ chmod u+x wait.sh
$ ./wait.sh
hello
$ echo $?
0

subprocess.callからシェルを実行する

次のPythonスクリプトを実行すると、startとまず表示され、2秒経過後に終了ステータス、endが表示される。

exec_sh.py

import subprocess
print('start')
returncode = subprocess.call(['./wait.sh'])
print(returncode)
print('end')
$ python exec_sh.py
start
# ここで2秒何も表示されない
hello
0
end

なお、終了ステータスが1の場合でもsubprocess.callで例外は投げられず、最後のendまで実行される。

subprocess.check_outputからシェルを実行する

subprocess.callの戻り値が終了ステータスであったのに対して、subprocess.check_outputの戻り値は出力になる。

import subprocess

print('start')
output = subprocess.check_output(['./wait.sh'])
print('end')

Pythonスクリプトを実行すると、echoで標準出力される内容がoutputに渡されていることがわかる。

$ python exec_sh.py
start
b'hello\n'
end

終了ステータスが1の場合には、subprocess.CalledProcessError例外が投げらるため、この例外をtry/exceptでハンドリングする必要がある。
例外のreturncodeに終了ステータス、outputに出力の内容が格納されている。

exec_sh.py

import subprocess

print('start')
try:
  output = subprocess.check_output(['./wait.sh'])
except subprocess.CalledProcessError as e:
  print(f"returncode:{e.returncode}, output:{e.output}")
print('end')

上記のコードを確認するためシェルを変更する。
helloと出力した後、終了ステータスが1で終わるようにする。
wait.shのファイル末尾に以下のコードを追加する。

exit 1

Pythonスクリプトを実行するとstartと表示され、2秒経過後にexceptに記載した終了ステータスとアウトプットが表示され、最後にendと表示される。

$ python exec_sh.py 
start
returncode:1, output:b'hello\n'
end

subprocess.runからシェルを実行する

推奨されてるsubprocess.runからシェルを実行する。

サブプロセスを起動するために推奨される方法は、すべての用法を扱える run() 関数を使用することです。 https://docs.python.org/ja/3/library/subprocess.html#using-the-subprocess-module

runメソッドの戻り値はCompletedProcessインスタンスである。
CompletedProcess.returncodeで終了ステータス、CompletedProcess.stdoutで標準出力、CompletedProcess.stderrで標準エラー出力を取得できる。 subprocess.Popenのようにcommunicateのような他のメソッドを呼び出す必要がなく、runメソッドの戻り値として一通り結果を取得できるため扱いやすい。

import subprocess

print('start')
completed_process = subprocess.run(['./wait.sh'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f'returncode: {completed_process.returncode},stdout: {completed_process.stdout},stderr: {completed_process.stderr}')
print('end')

実行するwait.shは標準出力、標準エラー出力、終了ステータスが確認できるよう、それぞれ値を設定する。

#!/bin/sh

sleep 2
echo 'hello'
echo 'err' >&2
exit 100

実行すると、一通り取得できていることを確認できる。

$ python exec_sh.py 
start
returncode: 100,stdout: b'hello\n',stderr: b'err\n'
end

subprocess.Popenからシェルを実行する

subprocess.callの戻り値が終了ステータス、subprocess.check_outputの戻り値が出力であったのに対して、subprocess.Popenの戻り値はPopenインスタンスである。

exec_sh.py

import subprocess

print('start')
popen_obj = subprocess.Popen(['./wait.sh'])
print(popen_obj)

print('end')

Pythonスクリプトを実行するとstart<subprocess.Popen object at 0x1075fbe10>endが続けて表示される。そして、2秒後helloが表示される。
subprocess.callsubprocess.check_outputで実行したときはwait.shの結果を待って、後続処理が動いていたが、subprocess.Popenwait.sh結果を待たずに後続処理が動く。
さて、このままだとwait.shの結果を受け取ることができない。

シェルの実行を待つ方法としてPopenインスタンスのwaitメソッド、communicateメソッドの2つが用意されている。 waitの方は標準出力に大量のデータを出力すると処理が止まってしまう。

stdout=PIPE や stderr=PIPE を使っていて、より多くのデータを受け入れるために OS のパイプバッファーをブロックしているパイプに子プロセスが十分な出力を生成した場合、デッドロックが発生します。これを避けるには Popen.communicate() を使用してください。https://docs.python.org/ja/3/library/subprocess.html#subprocess.Popen.wait

そのため、communicateメソッドを使う方法を見て行く。
communicateメソッドは戻り値として標準出力、標準エラー出力のタプル(stdout_data, stderr_data)を返す。
また、Popenインスタンスからはreturncodeが取得できる。

exec_sh.py

import subprocess

print('start')
popen_obj = subprocess.Popen(['./wait.sh'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout_data, stderr_data = popen_obj.communicate()
print(popen_obj.returncode)
print('stdout_data:', stdout_data, ' stderr_data:', stderr_data)
print('end')

実行した結果、シェルの実行を待ち、標準出力、標準エラー出力、終了ステータスの内容が取得できている。

$ python exec_sh.py 
start
100
stdout_data: b'hello\n'  stderr_data: b'err\n'
end

参考

https://docs.python.org/ja/3/library/subprocess.html#subprocess
https://stackoverflow.com/questions/38088631/what-is-a-practical-difference-between-check-call-check-output-call-and-popen-m/40768384
https://stackoverflow.com/questions/13332268/how-to-use-subprocess-command-with-pipes