サブプロセス Python
PythonのSubprocessについてのまとめノートです。
尚、この記事のコードを試す場合、最低でもPython version 3.5以上の環境をお勧めします。一部3.7以上必要なものもあります。
動作環境
name -srv
# Linux 5.4.0-121-generic #137-Ubuntu SMP Wed Jun 15 13:33:07 UTC 2022
python --version
# Python 3.8.5
Subprocessモジュールの概略
-
run() : サブプロセスを起動させるための関数。
- 内部でPopenクラスのオブジェクトを生成する
- 最も基本的な使用方法は、コマンドを文字列のリストとしてrun()関数の引数に渡す →コード例
- 引数のうち、timeout, input, check, capture_output以外はそのままPopenクラスのコンストラクタに渡される
- class Popen : サブプロセス生成、管理のためのクラス
- 直接このクラスオブジェクトを生成も可能
この記事では対象外
- 直接このクラスオブジェクトを生成も可能
- class CompletedProcess : 終了した子プロセスの情報を保持するクラス
- run()の戻り値として返ってくるオブジェクトのクラス
- exception
サンプルコード
実行方法
子プロセス、親プロセスの各スクリプトをそれぞれchild.py, parent.pyとする。
カレントディレクトリに存在し、以下のような実行を行うこととする。
ls
# child.py parent.py
python parent.py
parent.pyから、python child.py [オプション]
という形のコマンドをサブプロセスとして起動する。
子プロセス(共通): child.py
擬似的な子プロセス用のコード。
usage: child.py [-h] [-r RETCODE] [-d DURATION] [-e]
options:
-h, --help show this help message and exit
-r RETCODE, --retcode RETCODE
-d DURATION, --duration DURATION
-e, --exception
-
-r 整数
で終了ステータスを指定 (デフォルトは0)
0以外の場合、stderrに擬似的エラーメッセージ出力 -
-d 整数
で処理時間(今回はsleepするだけ)を指定 (デフォルトは5[sec]) -
-e
で例外を発生させるように指定 (デフォルトは例外無し)
#!/usr/bin/env python
import os, sys
from time import sleep
from argparse import ArgumentParser
def func(retcode=0, duration=5, exception=False):
print('child : PID={}, PPID={}'.format( os.getpid(), os.getppid()))
sleep(duration)
if retcode != 0:
print('ERR child : PID={}, PPID={}'.format(os.getpid(), os.getppid()),
file=sys.stderr)
if exception :
raise RuntimeError
print('child : I\'m done.')
return retcode
if __name__ == '__main__':
parser = ArgumentParser()
parser.add_argument('-r', '--retcode', type=int, default=0)
parser.add_argument('-d', '--duration', type=int, default=5)
parser.add_argument('-e', '--exception',action='store_true', default=False)
args = parser.parse_args()
print(__file__)
print("child :", vars(args))
sys.exit(func(**vars(args)))
argparse
については別途記事を書く予定。
親プロセス (実装例各種) : parent.py
共通部分
以降のサンプルコードは以下のモジュールを使用している。
import subprocess
import os
import sys
シンプルな呼び出し
run()
を使用してコマンドを呼び出す。引数に ['python', 'child.py' ]
を渡す。
def call_subproc(cmd):
print('parent : PID={}'.format(os.getpid()))
completeproc = subprocess.run(cmd)
print('parent : child\'s exit status', completeproc.returncode)
if __name__ == '__main__':
print(__file__)
# python child.py
# 終了ステータス=0、処理時間=5秒、例外無し
call_subproc( ['python', 'child.py' ])
戻り値としてCompletedProcessクラスのオブジェクト(completeproc)が返ってくる。
終了ステータス(completeproc.returncode)を確認することでエラーの有無を確認できる。
実行時の出力例
parent.py
parent : PID=21076
child.py
child : {'retcode': 0, 'duration': 5, 'exception': False}
child : PID=21109, PPID=21076
child : I'm done.
parent : child's exit status 0
子プロセスにbash
子プロセスはPythonコードである必要はない。
# 略
## 上記のcall_subproc()の定義を参照
if __name__ == '__main__':
print(__file__)
call_subproc( [ 'bash', '-c', 'echo $$ $PPID; echo ERR $$ $PPID 1>&2; exit 1; ' ])
出力例
parent.py
parent : PID=22196
22229 22196
ERR 22229 22196
parent : child's exit status 1
標準出力を/dev/null
へリダイレクト
stdout
にsubprocess.DEVNULL
を指定する。
def call_sub_devnull(cmd):
print('parent : PID={}'.format(os.getpid()))
completeproc = subprocess.run(cmd, stdout=subprocess.DEVNULL)
print('parent : child\'s exit status', completeproc.returncode)
if __name__ == '__main__':
print(__file__)
call_sub_devnull( ['python', 'child.py' ])
実行時の出力例
parent.py
parent : PID=21528
parent : child's exit status 0
子プロセスのstdout,stderr
子プロセスでエラー
子プロセスがstderrにエラーメッセージを出力し、ステータス=1で返ってくる場合を擬似的に再現。
capture_output
を使用することで子プロセスのstdoutやstderrの出力を受け取れる。デフォルトではバイトデータで渡されるため、text
を指定して文字列で受け取る。
注意:text
はPython version 3.7以降必要。
def call_sub_stderr():
print('parent : PID={}'.format(os.getpid()))
# python child.py -r 1
# 終了ステータス=1、処理時間=5秒、例外無し
completeproc = subprocess.run(
['python', 'child.py', '-r', '1' ],
capture_output=True,
text=True,
)
print('parent : child\'s exit status', completeproc.returncode)
print('parent : child\'s stdout ---')
print(completeproc.stdout)
print('parent : -------------------')
print('parent : child\'s stderr ---')
print(completeproc.stderr)
print('parent : -------------------')
if __name__ == '__main__':
print(__file__)
call_sub_stderr()
出力例
parent.py
parent : PID=21625
parent : child's exit status 1
parent : child's stdout ---
child.py
child : {'retcode': 1, 'duration': 5, 'exception': False}
child : PID=21658, PPID=21625
child : I'm done.
parent : -------------------
parent : child's stderr ---
ERR child : PID=21658, PPID=21625
parent : -------------------
子プロセスがPython例外
Pythonコードで、例外が発生してエラー終了する場合は例外のメッセージがstderrに出力される。
def call_sub_except():
print('parent : PID={}'.format(os.getpid()))
# python child.py -e
# 終了ステータス=0、処理時間=5秒、例外有り
#→例外発生により最終的には終了ステータスは1
completeproc = subprocess.run(
['python', 'child.py', '-e' ],
capture_output=True,
text=True,
)
print('parent : child\'s exit status', completeproc.returncode)
print('parent : child\'s stdout ---')
print(completeproc.stdout)
print('parent : -------------------')
print('parent : child\'s stderr ---')
print(completeproc.stderr)
print('parent : -------------------')
if __name__ == '__main__':
print(__file__)
call_sub_except()
出力例
parent.py
parent : PID=21866
parent : child's exit status 1
parent : child's stdout ---
child.py
child : {'retcode': 0, 'duration': 5, 'exception': True}
child : PID=21899, PPID=21866
parent : -------------------
parent : child's stderr ---
Traceback (most recent call last):
File "child.py", line 28, in <module>
sys.exit(func(**vars(args)))
File "child.py", line 13, in func
raise RuntimeError
RuntimeError
parent : -------------------
子プロセスのエラー時に親プロセスで例外を発生させる
check
を指定する。もしくは戻り値のオブジェクトでcheck_returncode()を呼び出すことでも可能。
子プロセスの終了ステータスが0以外の場合エラーとして検出される。
def call_sub_check():
print('parent : PID={}'.format(os.getpid()))
try:
# python child.py -r 1
# 終了ステータス=1、処理時間=5秒、例外無し
completeproc = subprocess.run(
['python', 'child.py', '-r', '1' ],
stderr=subprocess.PIPE,
text=True,
check=True
)
except subprocess.CalledProcessError as e:
# 子プロセスのコマンド
print('parent : ', e.cmd)
# 子プロセスの終了ステータス
print('parent : ', e.returncode)
print('parent : child\'s stderr ---')
# 子プロセスのstderr
print(e.stderr)
print('parent : -------------------')
if __name__ == '__main__':
print(__file__)
call_sub_check()
ここでは、stderrの出力をキャプチャして親プロセスで受け取る設定もいれている。
run()が正常終了せず、例外が発生した場合は、例外クラスのstderrやstdoutにキャプチャされた出力が入ってくる。
出力例
parent.py
parent : PID=21935
child.py
child : {'retcode': 1, 'duration': 5, 'exception': False}
child : PID=21968, PPID=21935
child : I'm done.
parent : ['python', 'child.py', '-r', '1']
parent : 1
parent : child's stderr ---
ERR child : PID=21968, PPID=21935
parent : -------------------
子プロセスのタイムアウト
子プロセスが指定時間待っても戻ってこない場合、タイムアウトの例外を発生させるように設定して呼び出せる。
def call_sub_timeout():
print('parent : PID={}'.format(os.getpid()))
try:
# python child.py -d 10
# 終了ステータス=1、処理時間=10秒、例外無し
# Timeoutを3secに指定
completeproc = subprocess.run(
['python', 'child.py', '-d', '10' ],
timeout=3)
except subprocess.TimeoutExpired as e:
print('parent :', e)
return
print('parent : child\'s exit status', completeproc.returncode)
if __name__ == '__main__':
print(__file__)
call_sub_timeout()
出力例
parent.py
parent : PID=22083
child.py
child : {'retcode': 0, 'duration': 10, 'exception': False}
child : PID=22116, PPID=22083
parent : Command '['python', 'child.py', '-d', '10']' timed out after 2.9998990160020185 seconds
注意事項
- ここではrun()の引数に渡す子プロセスのコマンドは
python child.py 引数...
の形で呼び出すコードになっているが、公式マニュアルではコマンドのフルパスを指定することを推奨している。具体的にはpython
の部分はshutil.which("python")
やsys.executable
などに置き換えると良い。