サブプロセスとは
そもそもターミナル(シェル)でコマンドを打つとどのようにしてプログラムが実行されるのだろうか?
実はシェルが自身のサブプロセスを作成するという形でプログラムは実行される.
そしてsubprocessモジュールとは文字通りサブプロセスを立ち上げるためのモジュールである.
このモジュールを用いることでpythonから他のプログラムを立ち上げたり,その出力を得たりすることができる.
シェルスクリプト的な使い方をしたいときに便利なモジュールである.
かつてはos.system
os.spawn
が使われていたが、subprocessはこれらを置き換えるためのモジュールである.
#TLDR
os.systemを置き換えるには
sts = subprocess.call("mycmd" + " myarg", shell=True)
とすればよい.
出力をとりたい場合,
proc = subprocess.run(["ls"],stdout = subprocess.PIPE, stderr = subprocess.PIPE)
print(proc.stdout.decode("utf8"))
print(proc.stderr.decode("utf8"))
とすればよい
プロセスとは
subprocessはプロセス関連のシステムコールを呼び出すためのモジュールといっていいだろう.subprocessの公式ドキュメントはあまり初心者にとって読みやすいものにはなっていない.これはUNIXのプロセスの概念をある程度知っていることが前提とされているためである.そこでプロセスとその周りの関連概念(fork, exec, pipe)について以下で解説する.
UNIX系のOSは,実行されているプログラムをプロセスとして管理している.さてあるプロセスから別のプログラムを実行したいということが良くある.OSはこのような機能を当然サポートしており,フォーク(fork)と呼ばれるシステムコールによって,プロセスは子プロセスを立ち上げることができる.子プロセスは,親プロセスのコピーとして作られ,その後システムコールexecによって異なるプログラムを実行するように塗り替えられる.従って子プロセスは親プロセスから環境変数などを受け継いでいる.
デフォルトでは子プロセスの出力を親プロセスは取得することはできない.しかしexecの前にプロセス間通信の手法(パイプ)を適切に設定してやることで,出力を得ることが可能になる.
パイプについて
パイプとはプロセス間の通信をするためのOSの機能である.簡単に言えばシェルのコマンドなどに現れる|
で使用されている機能である.システムコールpipeを呼ぶと,カーネル内にバッファが確保され,出力用と入力用の二つのファイルディスクリプタを得ることができる.(これをパイプを開くなどと形容することがある)ファイルディスクリプタとは開いたファイルの情報を管理しているの構造体へのインデックスである.この構造体はプロセスごとに管理されている.(デフォルトでは0が標準入力,1が標準出力,2が標準エラー出力.ファイルを開くときにシステムコールopenを呼ぶがこれで帰ってくるものでもある).パイプは基本的にはファイルなどと同様に書き込み・読み込みなどが可能である.(シークはできない)
execの前にpipeを呼んでおくことで,親プロセスと子プロセスは同じパイプにアクセスすることができるようになる.特に子プロセス側で標準出力をパイプにつなぎ変え,親プロセス側でパイプから読みだすことで,子プロセスの標準出力を取得できるようになる.
ただし”パイプ”という名前から連想されるような自動性はあまり無く,バッファのように読み出し・書き込みをベースとしているため下手をするとデッドロックを起こす.以下のことに注意したい.
- 読み込み側はEOFになるまで読み込み続けられる.読んだ分バッファから捨てられる.
- 書き込み側はバッファの限り書き込み続けられる.たくさん書きこんでしまうとバッファがいっぱいになってしまい.それ以上の書き込みがブロックされてしまい.デッドロックの原因になることがある.
- デッドロック問題はPopen.communicateで時間の制限を設定してやることで回避が可能なので,信頼性が求められる場合はよく考えて設定する必要がある.
詳しい解説はここなどを参照.Cで一度書いてみるとなんとなく意味が実感できるような気がする.
とりあえず動かす
subprocess.run(["ls"])
こんな感じで適当にやれば,シェルで実行するようなコマンドが実行できてしてしまう.しかし出力は拾えない(インタープリタ上で動かしていると問題ないが,ログをとったり,jupyter notebook上で実行するときに不便.).
なお普通にシェルで実行するようなコマンドを実行するときは以下のようにすればよい.
subprocess.run("ls",shell =True)
shell=True
にすると標準のシェルでコマンドを実行するようになるらしい.セキュリティ上のリスクになりうるので注意が必要.
出力ぐらいはpython側でとりたい
proc = subprocess.run(["ls"],stdout = subprocess.PIPE, stderr = subprocess.PIPE)
print(proc.stdout.decode("utf8"))
逆に出力は要らない場合
proc = subprocess.run(["ls"],stdout = subprocess.DEVNULL,stderr = subprocess.DEVNULL)
print(proc.stdout.decode("utf8"))
別のディレクトリで実行したい
proc = subprocess.run(["ls"],cwd = "~/hoge")
シェルスクリプトのパイプを実現する
p1 = subprocess.Popen(["dmesg"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "hda"], stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close() # SIGPIPE if p2 exits.
output = p2.communicate()[0]
実は普通にパイプ入りのコマンドも実行もできる
subprocess.check_output("dmesg | grep hda", shell=True)
リアルタイムで出力をとる.
p = subprocess.Popen(mycmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
for line in iter(p.stdout.readline,b''):
print(line.rstrip().decode("utf8"))
stderr = subprocess.STDOUT
とするのは,エラーもまとめて標準出力にリダイレクトするため.
iterの第二変数は, readline() が空文字列を返すまで出力を読み進めるという意味.rsripはprintでの改行と被るのを防ぐために行っている.
まとめ
とりあえず,思いついた使い方はこれぐらい.何か思いついたら追記します.基本的にサブプロセスに対してできることは何でもできる気がしている.ある程度慣れてきたら公式ドキュメントを読めば,やりたいことを実現する方法がわかる気がする.修正コメントなどお待ちしております
参考
公式ドキュメント
catching stdout in realtime from subprocess -Stack Overflow