Python 3のsubprocess.run()の使い方


このエントリーをはてなブックマークに追加

Python 3で外部コマンドを呼ぶにはsubprocessモジュールで提供されているrun()を使うことが推奨されるのですが、毎回使い方を調べてしまっているので自分用にまとめます。順を追っていかないと引数の意味を理解しづらいところがあるので、冗長ですが簡単な例から書いていきます。なお、run()が導入されたのはPython 3.5なので、それより前のバージョンでは使えません。CygwinPython 3.8.3で動作確認しています。

基本

呼び出される側のスクリプトとして、以下のようなhello.pyを用意します。これは標準出力に「こんにちは」、標準エラー出力に「こんばんは」と出力するだけのトイプログラムです。

import sys

print("こんにちは", file=sys.stdout)
print("こんばんは", file=sys.stderr)

さっそくhello.pyをsubprocess.run()`を使って呼び出してみます。もっともシンプルな呼び出し方は

res = subprocess.run(['python3', 'hello.py'])

ですが、これだとresの中には

CompletedProcess(args=['python3', 'hello.py'], returncode=0)

という情報しか入っていません。これでは肝心の標準出力結果、標準エラー出力結果は得られません。

標準出力結果、標準エラー出力結果を得るためには、以下のようにcapture_output=Trueをつけます(Python 3.7以降限定;Python 3.6以前では代わりにstdout=subprocess.PIPEstderr=subprocess.PIPE の2つを指定)。

res = subprocess.run(['python3', 'hello.py'], capture_output=True)
print("return code: {}".format(res.returncode))
print("captured stdout: {}".format(res.stdout.decode()))
print("captured stderr: {}".format(res.stderr.decode()))

出力結果は以下です。res.stdoutで標準出力結果を、res.stderr標準エラー出力結果を得られていることがわかります。

return code: 0
captured stdout: こんにちは

captured stderr: こんばんは

ここで注意なのは、res.stdoutres.stderrは、ともに文字列ではなくバイトデータであることです。なのでdecode()を呼んで文字列に変換してやる必要があります。

これは面倒なので、以下のようにtext=Trueをつけると、文字列が返ってくるようになり便利です(Python 3.7以降;Python 3.3 ~ 3.6では代わりにuniversal_newlines=Trueをつける)。

res = subprocess.run(['python3', 'hello.py'], capture_output=True, text=True)
print("return code: {}".format(res.returncode))
print("captured stdout: {}".format(res.stdout))  # こんにちは
print("captured stderr: {}".format(res.stderr))  # こんばんは

明に文字列のエンコード方法を指定したいときは、text=Trueの代わりにencoding='utf8'などとつけても良いようです。

標準出力と標準エラー出力とをまとめてキャプチャ

標準出力と標準エラー出力とをまとめてキャプチャするには、stdout=subprocess.PIPE, stderr=subprocess.STDOUT と指定します(参考:subprocess — Subprocess management — Python 3.9.5 documentationIf you wish to capture and combine both streams into one, use stdout=PIPE and stderr=STDOUT instead of capture_output.

res = subprocess.run(['python3', 'hello.py'],
                     stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                     text=True)
print("captured stdout & stderr: {}".format(res.stdout))

ただ、これには大きな欠点があります。標準出力、標準エラー出力それぞれの出力の順番が保存されずマージされてしまいます。上記の例では、

captured stdout & stderr: こんばんは
こんにちは

と、順序が逆に表示されてしまいました。

上記の例だけに限っていうと、python - How can I make subprocess get the stdout and stderr in order? - Stack Overflow にある通り、['python3', '-u', 'hello.py']-uオプションを入れてunbufferedを強制することで期待通りの順序になりました。しかし、一般論でいうと、順序を常に保存するかんたんな方法はなさそうでした。

中身の理解はできていませんが参考になりそうなリンクを張っておきます。

例外処理

呼び出したコマンドが成功したかどうかは、run()の戻り値のreturncodeを確認します。0なら成功、非0なら失敗です。

例として、実行すると異常終了するraise_error.pyを以下のように作成します。

import sys
sys.exit(1)

これの戻り値を確認する例が以下です。

res = subprocess.run(['python3', 'raise_error.py'])
print("return code: {}".format(res.returncode))  # 1が表示される

run()check=Trueを追加することで、呼び出したコマンドが失敗したときに例外を返すようにすることができます。

$ python3 subprocess_test.py
return code: 1
------------------------------------------------------------
Traceback (most recent call last):
  File "subprocess_test.py", line 46, in <module>
    res = subprocess.run(['python3', 'raise_error.py'], check=True)
  File "/usr/lib/python3.8/subprocess.py", line 512, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['python3', 'raise_error.py']' returned non-zero exit status 1.

check=Trueをつけるべきかどうかは趣味と場合によりそうです。

コマンドに引数を与える

呼び出される側のスクリプトとして、1からNまでの整数の和を返すsum_1_to_n.pyを用意します。nは標準入力で受け取る仕様です。

n = int(input())
print(n * (n + 1) // 2)

コンソールで

$ echo 10 | python3 sum_1_to_n.py

とするのと同じノリでsubprocess.run()を使う場合、コマンドをリストで表現する方法ではパイプ(|)を使えないので、以下のようにshell = Trueを使ってコマンドを文字列として表現する必要があります。

res = subprocess.run('echo 10 | python3 sum_1_to_n.py', shell=True, capture_output=True, text=True)
print("captured stdout: {}".format(res.stdout))  # 55と表示される

しかし、一般にshell=Trueと指定すると脆弱性が発生するので推奨されません。個人のスクリプトでは便利なのでよくやってしまいますが…

上記の例だと、input=10というのを使うと、標準入力に10を指定したのと同じになってパイプを回避できます。

res = subprocess.run(['python3', 'sum_1_to_n.py'], input='10', text=True, capture_output=True)
print("captured stdout: {}".format(res.stdout))

パイプを回避するより一般的な例が、 この処理Pythonでどう書く? - エムスリーテックブログ の「パイプを使う」に記載されています。subprocess.run()より低レベルな関数subprocess.Popen()を使うのですが、私は理解できていません。

その他の便利オプション

  • cwdオプションによりコマンド実行時のワーキングディレクトリを変更できます
  • envオプションによりコマンド実行時の環境変数を変更できます

参考URL