サブプロセスを使用してPython3で外部プログラムを実行する方法

提供:Dev Guides
移動先:案内検索

著者はCOVID-19救済基金を選択し、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

Python 3には、外部プログラムを実行し、Pythonコードでそれらの出力を読み取るためのサブプロセスモジュールが含まれています。

Pythonコード内からコンピューター上で別のプログラムを使用する場合は、subprocessが役立つ場合があります。 たとえば、Pythonコード内から git を呼び出して、gitバージョン管理で追跡されているプロジェクト内のファイルを取得したい場合があります。 コンピューターでアクセスできるプログラムはすべてsubprocessで制御できるため、ここに示す例は、Pythonコードから呼び出す可能性のあるすべての外部プログラムに適用できます。

subprocessにはいくつかのクラスと関数が含まれていますが、このチュートリアルでは、subprocessの最も便利な関数の1つであるsubprocess.runについて説明します。 そのさまざまな使用法と主なキーワード引数を確認します。

前提条件

このチュートリアルを最大限に活用するには、Python3でのプログラミングにある程度精通していることをお勧めします。 必要な背景情報については、次のチュートリアルを確認できます。

外部プログラムの実行

subprocess.run関数を使用して、Pythonコードから外部プログラムを実行できます。 ただし、最初に、subprocessおよびsysモジュールをプログラムにインポートする必要があります。

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "print('ocean')"])

これを実行すると、次のような出力が表示されます。

Outputocean

この例を確認してみましょう。

  • sys.executable は、プログラムが最初に呼び出されたPython実行可能ファイルへの絶対パスです。 たとえば、sys.executable/usr/local/bin/pythonのようなパスである可能性があります。
  • subprocess.runには、実行しようとしているコマンドのコンポーネントで構成される文字列のリストが表示されます。 渡す最初の文字列はsys.executableであるため、subprocess.runに新しいPythonプログラムを実行するように指示しています。
  • -cコンポーネントは、pythonコマンドラインオプションであり、Pythonプログラム全体を含む文字列を渡して実行できます。 この例では、文字列oceanを出力するプログラムを渡します。

subprocess.runに渡すリスト内の各エントリは、スペースで区切られていると考えることができます。 たとえば、[sys.executable, "-c", "print('ocean')"]は大まかに/usr/local/bin/python -c "print('ocean')"に変換されます。 subprocessは、基になるオペレーティングシステムで実行しようとする前に、コマンドのコンポーネントを自動的に引用することに注意してください。たとえば、スペースを含むファイル名を渡すことができます。

警告:信頼できない入力をsubprocess.runに渡さないでください。 subprocess.runにはコンピューター上で任意のコマンドを実行する機能があるため、悪意のある攻撃者がそれを使用して予期しない方法でコンピューターを操作する可能性があります。


外部プログラムからの出力のキャプチャ

subprocess.runを使用して外部プログラムを呼び出すことができるようになったので、そのプログラムからの出力をキャプチャする方法を見てみましょう。 たとえば、このプロセスは、git ls-filesを使用して、現在バージョン管理下に保存されているすべてのファイルを出力する場合に役立ちます。

注:このセクションに示されている例には、Python3.7以降が必要です。 特に、capture_outputおよびtextキーワード引数は、2018年6月にリリースされたPython3.7で追加されました。


前の例に追加しましょう:

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-c", "print('ocean')"], capture_output=True, text=True
)
print("stdout:", result.stdout)
print("stderr:", result.stderr)

このコードを実行すると、次のような出力が返されます。

Outputstdout: ocean

stderr:

この例は、最初のセクションで紹介した例とほぼ同じです。oceanを印刷するためのサブプロセスをまだ実行しています。 ただし、重要なのは、capture_output=Trueおよびtext=Trueキーワード引数をsubprocess.runに渡すことです。

subprocess.runは、resultにバインドされているsubprocess.CompletedProcessオブジェクトを返します。 subprocess.CompletedProcessオブジェクトには、外部プログラムの終了コードとその出力に関する詳細が含まれています。 capture_output=Trueは、result.stdoutおよびresult.stderrが外部プログラムからの対応する出力で埋められることを保証します。 デフォルトでは、result.stdoutresult.stderrはバイトとしてバインドされますが、text=Trueキーワード引数は、代わりにバイトを文字列にデコードするようにPythonに指示します。

出力セクションでは、stdoutocean(およびprintが暗黙的に追加する末尾の改行)であり、stderrはありません。

stderrの空でない値を生成する例を試してみましょう。

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-c", "raise ValueError('oops')"], capture_output=True, text=True
)
print("stdout:", result.stdout)
print("stderr:", result.stderr)

このコードを実行すると、次のような出力が返されます。

Outputstdout:
stderr: Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: oops

このコードは、ValueErrorをすぐに発生させるPythonサブプロセスを実行します。 最終的なresultを検査すると、stdoutには何も表示されず、stderrにはValueErrorTracebackが表示されます。 これは、デフォルトでPythonが未処理の例外のTracebackstderrに書き込むためです。

不正な終了コードで例外を発生させる

実行するプログラムが不正な終了コードで終了した場合に、例外を発生させると便利な場合があります。 ゼロコードで終了するプログラムは成功したと見なされますが、ゼロ以外のコードで終了するプログラムはエラーが発生したと見なされます。 例として、このパターンは、実際にはgitリポジトリではないディレクトリでgit ls-filesを実行した場合に例外を発生させたい場合に役立ちます。

check=Trueキーワード引数をsubprocess.runに使用して、外部プログラムがゼロ以外の終了コードを返した場合に例外を発生させることができます。

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "raise ValueError('oops')"], check=True)

このコードを実行すると、次のような出力が返されます。

OutputTraceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: oops
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/subprocess.py", line 512, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['/usr/local/bin/python', '-c', "raise ValueError('oops')"]' returned non-zero exit status 1.

この出力は、エラーが発生したサブプロセスを実行したことを示しています。このサブプロセスは、端末のstderrに出力されます。 次に、subprocess.runは、メインのPythonプログラムで私たちに代わってsubprocess.CalledProcessErrorを忠実に作成しました。

または、subprocessモジュールには、 subprocess.CompletedProcess.check_returncode メソッドも含まれています。これを呼び出すと、同様の効果が得られます。

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "raise ValueError('oops')"])
result.check_returncode()

このコードを実行すると、次のようになります。

OutputTraceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: oops
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/subprocess.py", line 444, in check_returncode
    raise CalledProcessError(self.returncode, self.args, self.stdout,
subprocess.CalledProcessError: Command '['/usr/local/bin/python', '-c', "raise ValueError('oops')"]' returned non-zero exit status 1.

check=Truesubprocess.runに渡さなかったため、プログラムがゼロ以外のコードで終了した場合でも、subprocess.CompletedProcessインスタンスをresultに正常にバインドできました。 ただし、result.check_returncode()を呼び出すと、完了したプロセスが不正なコードで終了したことが検出されるため、subprocess.CalledProcessErrorが発生します。

タイムアウトを使用してプログラムを早期に終了する

subprocess.runには、timeout引数が含まれており、実行に時間がかかりすぎる場合に外部プログラムを停止できます。

import subprocess
import sys

result = subprocess.run([sys.executable, "-c", "import time; time.sleep(2)"], timeout=1)

このコードを実行すると、次のような出力が返されます。

OutputTraceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/subprocess.py", line 491, in run
    stdout, stderr = process.communicate(input, timeout=timeout)
  File "/usr/local/lib/python3.8/subprocess.py", line 1024, in communicate
    stdout, stderr = self._communicate(input, endtime, timeout)
  File "/usr/local/lib/python3.8/subprocess.py", line 1892, in _communicate
    self.wait(timeout=self._remaining_time(endtime))
  File "/usr/local/lib/python3.8/subprocess.py", line 1079, in wait
    return self._wait(timeout=timeout)
  File "/usr/local/lib/python3.8/subprocess.py", line 1796, in _wait
    raise TimeoutExpired(self.args, timeout)
subprocess.TimeoutExpired: Command '['/usr/local/bin/python', '-c', 'import time; time.sleep(2)']' timed out after 0.9997982999999522 seconds

実行しようとしたサブプロセスは、time.sleep関数を使用して2秒間スリープしました。 ただし、timeout=1キーワード引数をsubprocess.runに渡して、1秒後にサブプロセスをタイムアウトしました。 これは、subprocess.runへの呼び出しが最終的にsubprocess.TimeoutExpired例外を発生させた理由を説明しています。

subprocess.runへのtimeoutキーワード引数は概算であることに注意してください。 Pythonは、timeout秒の後にサブプロセスを強制終了するために最善を尽くしますが、必ずしも正確であるとは限りません。

プログラムへの入力の受け渡し

プログラムは、入力がstdinを介して渡されることを期待する場合があります。

subprocess.runへのinputキーワード引数を使用すると、サブプロセスのstdinにデータを渡すことができます。 例えば:

import subprocess
import sys

result = subprocess.run(
    [sys.executable, "-c", "import sys; print(sys.stdin.read())"], input=b"underwater"
)

このコードを実行すると、次のような出力が返されます。

Outputunderwater

この場合、バイトunderwaterinputに渡しました。 ターゲットサブプロセスは、 sys.stdin を使用して、渡されたstdinunderwater)を読み取り、出力に出力しました。

inputキーワード引数は、複数のsubprocess.run呼び出しを連鎖させて、あるプログラムの出力を別のプログラムの入力として渡す場合に役立ちます。

結論

subprocessモジュールは、Python標準ライブラリの強力な部分であり、外部プログラムを実行してその出力を簡単に検査できます。 このチュートリアルでは、subprocess.runを使用して外部プログラムを制御し、入力をプログラムに渡し、出力を解析し、リターンコードを確認する方法を学びました。

subprocessモジュールは、このチュートリアルでは取り上げなかった追加のクラスとユーティリティを公開します。 ベースラインができたので、サブプロセスモジュールのドキュメントを使用して、他の利用可能なクラスとユーティリティについて詳しく知ることができます。