subprocess – プロセスを生成して連携する

目的:新しくプロセスを生成してプロセスと通信する
利用できるバージョン:2.4 以上

subprocess モジュールは新しくプロセスを生成して、そのプロセスを扱う一貫したインタフェースを提供します。それは従来からある他のモジュールよりも高レベルなインタフェースを提供します。そして os.system(), os.spawn*(), os.popen*(), popen2.*()commands.*() のような従来の関数の置き換えを目的としています。 subprocess モジュールと他のモジュールとの比較を分かり易くするために ospopen を使用したサンプルを再作成して紹介します。

subprocess モジュールは Popen とそのクラスを使用する複数のラッパ関数を定義します。 Popen のコンストラクタは新たなプロセス生成を簡単にする複数の引数を受け取り、パイプを経由してその親プロセスと通信します。それは他のモジュールの置き換えられる全ての機能や関数とそれ以上の機能を提供します。どんな利用方法に対応できるように API が構成されています。そして、必要な(ファイルディスクリプタやパイプのクローズを保証するような)オーバヘッドの追加ステップの多くは、そのアプリケーションコードが独立して扱わずに “ビルトイン” です。

Note

API は大雑把には同じですが Unix と Windows 環境の違いで低レイヤの実装は若干違います。本稿で紹介する全てのサンプルコードは Mac OS X でテストされています。非 Unix 環境ではその実行結果が変わるでしょう。

外部コマンドを実行する

実行する外部コマンドのプロセスとやり取りしない os.system() で実行されるような処理は call() 関数を使用してください。

import subprocess

# シンプルコマンド
subprocess.call(['ls', '-1'], shell=True)

コマンドライン引数は文字列のリストとして渡されます。それはエスケープする必要性やシェルが解釈する可能性がある他の特殊文字を避けます。

$ python subprocess_os_system.py
__init__.py
index.rst
interaction.py
repeater.py
signal_child.py
signal_parent.py
subprocess_check_call.py
subprocess_check_output.py
subprocess_check_output_error.py
subprocess_check_output_error_trap_output.py
subprocess_os_system.py
subprocess_pipes.py
subprocess_popen2.py
subprocess_popen3.py
subprocess_popen4.py
subprocess_popen_read.py
subprocess_popen_write.py
subprocess_shell_variables.py
subprocess_signal_parent_shell.py
subprocess_signal_setsid.py

shell 引数に True をセットすると subprocess はシェルを介してプロセスを生成して、コマンド実行するようにそのプロセスに伝えます。デフォルトでは、コマンドは直接実行します。

import subprocess

# シェル変数を展開したコマンド
subprocess.call('echo $HOME', shell=True)

シェルを介することで、コマンド文字列内の変数、glob パターンやその他の特殊なシェル機能がコマンドの実行前に処理されることになります。

$ python subprocess_shell_variables.py
/Users/dhellmann

エラーハンドリング

call() の返り値はそのプログラムの終了コードです。呼び出し側はエラーを検出するためにその終了コードを調べる責任があります。 check_call() 関数はその終了コードを確認すること以外は call() のように動作します。そして、もしエラーが発生した場合 CalledProcessError 例外が発生します。

import subprocess

subprocess.check_call(['false'])

false コマンドは常にゼロではないステータスコードで終了します。それは check_call() がエラーとして解釈します。

$ python subprocess_check_call.py
Traceback (most recent call last):
  File "subprocess_check_call.py", line 11, in <module>
    subprocess.check_call(['false'])
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.
7/subprocess.py", line 511, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['false']' returned non-zero e
xit status 1

出力を取得する

call() が生成するプロセスの標準入力や標準出力のチャンネルは親の入出力に束縛されます。これは呼び出すプログラムはコマンドの出力を取得できないことを意味します。後続の処理のためにその出力を取得するために check_output() を使用してください。

import subprocess

output = subprocess.check_output(['ls', '-1'])
print 'Have %d bytes in output' % len(output)
print output

                                  

ls -1 コマンドは正常に実行するので、標準出力へ表示するテキストが取得されて返されます。

$ python subprocess_check_output.py
Have 462 bytes in output
__init__.py
index.rst
interaction.py
repeater.py
signal_child.py
signal_parent.py
subprocess_check_call.py
subprocess_check_output.py
subprocess_check_output_error.py
subprocess_check_output_error_trap_output.py
subprocess_os_system.py
subprocess_pipes.py
subprocess_popen2.py
subprocess_popen3.py
subprocess_popen4.py
subprocess_popen_read.py
subprocess_popen_write.py
subprocess_shell_variables.py
subprocess_signal_parent_shell.py
subprocess_signal_setsid.py

このスクリプトはサブシェル内で一連のコマンドを実行します。メッセージはコマンドがエラーコードで終了する前に標準出力と標準エラーに送られます。

import subprocess

output = subprocess.check_output(
    'echo to stdout; echo to stderr 1>&2; exit 1',
    shell=True,
    )
print 'Have %d bytes in output' % len(output)
print output

                                  

標準エラーへのメッセージはコンソールに表示されますが、標準出力へのメッセージは隠蔽されています。

$ python subprocess_check_output_error.py
to stderr
Traceback (most recent call last):
  File "subprocess_check_output_error.py", line 14, in <module>
    shell=True,
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.
7/subprocess.py", line 544, in check_output
    raise CalledProcessError(retcode, cmd, output=output)
subprocess.CalledProcessError: Command 'echo to stdout; echo to stderr
 1>&2; exit 1' returned non-zero exit status 1

エラーメッセージが check_output() を経由して実行されるコマンドからコンソールに書き込まれないようにするために stderr パラメータに定数 STDOUT をセットします。

import subprocess

output = subprocess.check_output(
    'echo to stdout; echo to stderr 1>&2; exit 1',
    shell=True,
    stderr=subprocess.STDOUT,
    )
print 'Have %d bytes in output' % len(output)
print output

                                  

いま、そのエラーと標準出力のチャンネルは一緒にまとめられます。そのため、コマンドがエラーメッセージを表示するなら、そのメッセージは取得されてコンソールには送られません。

$ python subprocess_check_output_error_trap_output.py
Traceback (most recent call last):
  File "subprocess_check_output_error_trap_output.py", line 15, in <mo
dule>
    stderr=subprocess.STDOUT,
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.
7/subprocess.py", line 544, in check_output
    raise CalledProcessError(retcode, cmd, output=output)
subprocess.CalledProcessError: Command 'echo to stdout; echo to stderr
 1>&2; exit 1' returned non-zero exit status 1

直接的にパイプと連携する

stdin, stdoutstderr に違う引数を渡すことで os.popen() に類似した変形処理を行うことができます。

popen

プロセスを実行してその全出力を読むには stdoutPIPE をセットして communicate() を呼び出します。

import subprocess

print '\nread:'
proc = subprocess.Popen(['echo', '"to stdout"'], 
                        stdout=subprocess.PIPE,
                        )
stdout_value = proc.communicate()[0]
print '\tstdout:', repr(stdout_value)

これは、その読み込みが Popen インスタンスによって内部的に管理されること以外は popen() の動作とよく似ています。

$ python subprocess_popen_read.py

read:
        stdout: '"to stdout"\n'

呼び出すプログラムからパイプにデータを書き込めるようにするには stdinPIPE をセットしてください。

import subprocess

print '\nwrite:'
proc = subprocess.Popen(['cat', '-'],
                        stdin=subprocess.PIPE,
                        )
proc.communicate('\tstdin: to stdin\n')

一度にプロセスの標準入力チャンネルへデータを送ることは communicate() へデータを渡します。これは popen()'w' モードで使用するのによく似ています。

$ python -u subprocess_popen_write.py

write:
        stdin: to stdin

popen2

読み書きのために Popen インスタンスを設定するには、前述したテクニックを組み合わせてください。

import subprocess

print '\npopen2:'

proc = subprocess.Popen(['cat', '-'],
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        )
stdout_value = proc.communicate('through stdin to stdout')[0]
print '\tpass through:', repr(stdout_value)

これは popen2() によく似たパイプを設定します。

$ python -u subprocess_popen2.py

popen2:
        pass through: 'through stdin to stdout'

popen3

さらに popen3() のように stdout や stderr の両方のストリームを監視することもできます。

import subprocess

print '\npopen3:'
proc = subprocess.Popen('cat -; echo "to stderr" 1>&2',
                        shell=True,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.PIPE,
                        )
stdout_value, stderr_value = proc.communicate('through stdin to stdout')
print '\tpass through:', repr(stdout_value)
print '\tstderr      :', repr(stderr_value)

stderr からの読み込みは stdout と同じです。 PIPE を渡すことはそのチャンネルにアタッチするために Popen へ伝えます。そして communicate() は返される前にそこから全てのデータを読み込みます。

$ python -u subprocess_popen3.py

popen3:
        pass through: 'through stdin to stdout'
        stderr      : 'to stderr\n'

popen4

プロセスの標準出力チャンネルへそのプロセスのエラー出力を繋ぐには、 PIPE ではなく stderrSTDOUT をセットしてください。

import subprocess

print '\npopen4:'
proc = subprocess.Popen('cat -; echo "to stderr" 1>&2',
                        shell=True,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        stderr=subprocess.STDOUT,
                        )
stdout_value, stderr_value = proc.communicate('through stdin to stdout\n')
print '\tcombined output:', repr(stdout_value)
print '\tstderr value   :', repr(stderr_value)

この方法で出力を組み合わせることは popen4() の動作方法によく似ています。

$ python -u subprocess_popen4.py

popen4:
        combined output: 'through stdin to stdout\nto stderr\n'
        stderr value   : None

パイプのセグメントへ接続する

独立した Popen インスタンスを作成して、その入力と出力を一緒に繋ぐことで、複数のコマンドは Unix シェルが扱うのとよく似た方法で pipeline に接続することができます。ある Popen インスタンスの stdout 属性は、 PIPE 定数の代わりにパイプラインの次の stdin 属性として使用されます。最終的な出力はパイプラインの最後のコマンドの stdout ハンドラからの読み込みます。

import subprocess

cat = subprocess.Popen(['cat', 'index.rst'], 
                        stdout=subprocess.PIPE,
                        )

grep = subprocess.Popen(['grep', '.. include::'],
                        stdin=cat.stdout,
                        stdout=subprocess.PIPE,
                        )

cut = subprocess.Popen(['cut', '-f', '3', '-d:'],
                        stdin=grep.stdout,
                        stdout=subprocess.PIPE,
                        )

end_of_pipe = cut.stdout

print 'Included files:'
for line in end_of_pipe:
    print '\t', line.strip()

このサンプルは cat index.rst | grep ".. include" | cut -f 3 -d: のコマンドラインを再現します。それは本セクションの reStructuredText ソースファイルを読み込み、全行から他のファイルの “include” を探して、そのファイル名のみを表示します。

$ python -u subprocess_pipes.py
Included files:
        subprocess_os_system.py
        subprocess_shell_variables.py
        subprocess_check_call.py
        subprocess_check_output.py
        subprocess_check_output_error.py
        subprocess_check_output_error_trap_output.py
        subprocess_popen_read.py
        subprocess_popen_write.py
        subprocess_popen2.py
        subprocess_popen3.py
        subprocess_popen4.py
        subprocess_pipes.py
        repeater.py
        interaction.py
        signal_child.py
        signal_parent.py
        subprocess_signal_parent_shell.py
        subprocess_signal_setsid.py

別のコマンドと相互にやり取りする

上述した全てのサンプルは制限のあるプロセス間のやり取りを前提としていました。 communicate() メソッドは全ての出力を読み込み、値を返す前に子プロセスの終了を待ちます。また Popen インスタンスが使用する個々のパイプハンドラに読み書きすることもできます。標準入力から読み込んで標準出力へ書き込むシンプルな echo プログラムでこのことを説明します。

import sys

sys.stderr.write('repeater.py: starting\n')
sys.stderr.flush()

while True:
    next_line = sys.stdin.readline()
    if not next_line:
        break
    sys.stdout.write(next_line)
    sys.stdout.flush()

sys.stderr.write('repeater.py: exiting\n')
sys.stderr.flush()

プログラムの開始時と終了時に repeater.py スクリプトが標準エラーに書き込みます。この情報は子プロセスのライフタイムを表すために使用されます。

次の標準入力と標準出力のやり取りのサンプルは違った方法で Popen インスタンスが所有する標準入力と標準出力のファイルハンドラを使用します。最初のサンプルでは、10個の数値リストからプロセスの標準入力へ書き込んだ後、その次の行で標準出力から読み返されます。2番目のサンプルでは、同じ10個の数値リストから書き込まれますが communicate() を使用して全ての出力をまとめて読み込みます。

import subprocess

print 'One line at a time:'
proc = subprocess.Popen('python repeater.py', 
                        shell=True,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        )
for i in range(10):
    proc.stdin.write('%d\n' % i)
    output = proc.stdout.readline()
    print output.rstrip()
remainder = proc.communicate()[0]
print remainder

print
print 'All output at once:'
proc = subprocess.Popen('python repeater.py', 
                        shell=True,
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        )
for i in range(10):
    proc.stdin.write('%d\n' % i)

output = proc.communicate()[0]
print output

ループ毎に標準エラー出力の "repeater.py: exiting" 行が違う場所に出力されます。

$ python -u interaction.py
One line at a time:
repeater.py: starting
0
1
2
3
4
5
6
7
8
9
repeater.py: exiting


All output at once:
repeater.py: starting
repeater.py: exiting
0
1
2
3
4
5
6
7
8
9

プロセス間でシグナルを送る

os のサンプルは os.fork() や os.kill() を使用してシグナルを送る デモも含みます。各 Popen インスタンスは子プロセスのプロセス ID と一緒に pid 属性を提供するので subprocess でよく似た処理を行うことができます。例えば、親プロセスによって実行される子プロセスのために次のスクリプトを使用します。

import os
import signal
import time
import sys

pid = os.getpid()
received = False

def signal_usr1(signum, frame):
    "Callback invoked when a signal is received"
    global received
    received = True
    print 'CHILD %6s: Received USR1' % pid
    sys.stdout.flush()

print 'CHILD %6s: Setting up signal handler' % pid
sys.stdout.flush()
signal.signal(signal.SIGUSR1, signal_usr1)
print 'CHILD %6s: Pausing to wait for signal' % pid
sys.stdout.flush()
time.sleep(3)

if not received:
    print 'CHILD %6s: Never received signal' % pid

親プロセスで結合されます。

import os
import signal
import subprocess
import time
import sys

proc = subprocess.Popen(['python', 'signal_child.py'])
print 'PARENT      : Pausing before sending signal...'
sys.stdout.flush()
time.sleep(1)
print 'PARENT      : Signaling child'
sys.stdout.flush()
os.kill(proc.pid, signal.SIGUSR1)

その結果出力は次のようになります。

$ python signal_parent.py
PARENT      : Pausing before sending signal...
CHILD  71970: Setting up signal handler
CHILD  71970: Pausing to wait for signal
PARENT      : Signaling child
CHILD  71970: Received USR1

プロセスグループ / セッション

Unix 環境ではプロセスツリーという概念があり Popen で生成したプロセスがサブプロセスを生成する場合、そういった子プロセスは親プロセスからのシグナルを決して受け取りません。例えば SIGINTSIGTERM を送ることでその子プロセスを終了させることが難しいということになります。

import os
import signal
import subprocess
import tempfile
import time
import sys

script = '''#!/bin/sh
echo "Shell script in process $$"
set -x
python signal_child.py
'''
script_file = tempfile.NamedTemporaryFile('wt')
script_file.write(script)
script_file.flush()

proc = subprocess.Popen(['sh', script_file.name], close_fds=True)
print 'PARENT      : Pausing before sending signal to child %s...' % proc.pid
sys.stdout.flush()
time.sleep(1)
print 'PARENT      : Signaling child %s' % proc.pid
sys.stdout.flush()
os.kill(proc.pid, signal.SIGUSR1)
time.sleep(3)

シグナルを送るために使用された pid はシグナルを待つシェルスクリプトの子プロセスの pid と違っています。というのは、このサンプルは3つの独立したプロセスが相互に関連して存在します。

  1. subprocess_signal_parent_shell.py
  2. スクリプトを実行する Unix シェルプロセスをメインプログラムが作成する
  3. signal_child.py
$ python subprocess_signal_parent_shell.py
PARENT      : Pausing before sending signal to child 71973...
Shell script in process 71973
+ python signal_child.py
CHILD  71974: Setting up signal handler
CHILD  71974: Pausing to wait for signal
PARENT      : Signaling child 71973
CHILD  71974: Never received signal

この問題の解決方法は、親プロセスと一緒にシグナルを受け取るために子プロセスと連携する プロセスグループ を使用することです。プロセスグループはカレントプロセスのプロセス ID に対して “セッション ID” をセットする os.setsid() で作成されます。全ての子プロセスはそのセッション ID を継承します。そして Popen とその子孫によって作成されたシェルのみにセッション ID をセットすべきなので os.setsid() を親プロセスで呼び出してはいけません。その代わり、その関数は preexec_fn 引数として Popen へ渡されます。そのため、生成した新しいプロセスがシェルを実行するために exec() を呼び出す前に、そのプロセス内部で fork() した後で実行されます。

import os
import signal
import subprocess
import tempfile
import time
import sys

script = '''#!/bin/sh
echo "Shell script in process $$"
set -x
python signal_child.py
'''
script_file = tempfile.NamedTemporaryFile('wt')
script_file.write(script)
script_file.flush()

proc = subprocess.Popen(['sh', script_file.name], 
                        close_fds=True,
                        preexec_fn=os.setsid,
                        )
print 'PARENT      : Pausing before sending signal to child %s...' % proc.pid
sys.stdout.flush()
time.sleep(1)
print 'PARENT      : Signaling process group %s' % proc.pid
sys.stdout.flush()
os.killpg(proc.pid, signal.SIGUSR1)
time.sleep(3)

イベントの順番は次のようになります。

  1. 親プログラムが Popen をインスタンス化する。
  2. Popen インスタンスが新しいプロセスを fork する。
  3. 新しいプロセスが os.setsid() を実行する。
  4. 新しいプロセスがシェルを開始するために exec() を実行する。
  5. シェルがシェルスクリプトを実行する。
  6. シェルスクリプトが再度 fork して、そのプロセスが Python を exec する。
  7. Python は signal_child.py を実行する。
  8. 親プログラムはシェルの pid を使用するプロセスグループへシグナルを送る。
  9. シェルと Python のプロセスグループはシグナルを受け取る。シェルはそのシグナルを無視する。Python はシグナルハンドラを実行する。

プロセスグループへシグナルを送るには Popen インスタンスからの pid 値を用いて os.killpg() を使用してください。

$ python subprocess_signal_setsid.py
PARENT      : Pausing before sending signal to child 71977...
Shell script in process 71977
+ python signal_child.py
CHILD  71978: Setting up signal handler
CHILD  71978: Pausing to wait for signal
PARENT      : Signaling process group 71977
CHILD  71978: Received USR1

See also

subprocess
本モジュールの標準ライブラリドキュメント
os
多くの関数が非推奨ではありますが os モジュールのプロセスと連携する関数は既存コードに広く使用されています
UNIX SIgnals and Process Groups
UNIX シグナル操作、プロセスグループの動作方法の優れた解説があります
Advanced Programming in the UNIX(R) Environment
複数プロセスでのシグナル操作、重複ファイルディスクリプタのクローズ等の扱いについて説明します
pipes
標準ライブラリの Unix シェルコマンドラインテンプレート
Bookmark and Share