Python subprocess.Popenの使い方|run()との違い・stdout・communicate・timeout

Pythonのsubprocess.Popenは、外部コマンドを起動したらすぐに制御を返し、実行中のプロセスに対して出力の逐次読み取り・入力・待機・停止ができるクラスです。終わるまで待って結果をまとめて受け取るだけならsubprocess.run()で足り、Popenは「待たずに動かしたい」「1行ずつ読みたい」「途中で止めたい」ときに使います。

この記事では、Popenの基本(poll・wait)、communicateでのstdout・stderrの受け取り方、出力をリアルタイムに読む方法、timeout・terminate・kill、shell=True、パイプでのつなぎ方、そしてrun()との違いを、実行結果つきで説明します。コードはmacOS上のPython 3.12で実行しました(lsやsleepなどのコマンドはmacOS・Linuxのものです。Windowsでは別のコマンドに置き換えてください)。

subprocess.Popenとrun()の違い

Pythonの公式ドキュメントは、サブプロセスの起動にはまずrun()を使い、それで扱えない高度な場合に土台のPopenを直接使うよう案内しています(subprocess — サブプロセス管理)。run()も内部ではPopenを使っています。

やりたいこと 使うもの
コマンドを実行して、終わったら結果をまとめて受け取る run()
終了コードが0以外ならエラーにしたい run(check=True)
起動したまま、Python側で別の処理を続けたい Popen
出力を1行ずつリアルタイムに処理したい Popen
複数のコマンドを同時に動かしたい Popen
実行中のプロセスを途中で止めたい Popen
コマンド同士をパイプでつなぎたい Popen

run()の使い方はPython subprocess.run()とは?外部コマンドを実行する方法で解説しています。この記事はPopenで初めてできることに絞ります。

Popenの基本:起動するとすぐ戻る(poll・wait)

import subprocess

p = subprocess.Popen(["sleep", "0.3"])
print("起動直後 poll():", p.poll())
p.wait()
print("wait後 poll():", p.poll(), "/ returncode:", p.returncode)

実行結果:

起動直後 poll(): None
wait後 poll(): 0 / returncode: 0
メソッド・属性 動き
poll() 終わっていれば終了コード、実行中ならNoneを返す。待たない
wait() 終わるまで待ち、終了コードを返す。timeout=も指定できる
returncode 終了コード。終わる前はNone
pid 起動したプロセスのID

poll()のNoneは「まだ動いている」、0は「正常に終わった」です。if not p.poll():のように書くと両方が同じ扱いになってしまうので、if p.poll() is None:と書いてください。

存在しないコマンドを渡すと、起動の時点でFileNotFoundError: [Errno 2] No such file or directory: 'no-such-command'になります。

stdout・stderrを受け取る:communicate()

出力を受け取るにはstdout=subprocess.PIPE(エラー出力も欲しければstderr=subprocess.PIPE)を指定し、communicate()で読みます。text=Trueを付けると文字列(str)で、付けないとバイト列(bytes)で返ります。

import subprocess

p = subprocess.Popen(["ls", "/no/such/dir"],
                     stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
out, err = p.communicate()
print("stdout:", repr(out))
print("stderr:", err.strip())
print("returncode:", p.returncode)

実行結果:

stdout: ''
stderr: ls: /no/such/dir: No such file or directory
returncode: 1

communicate()は終わるまで待ってから、標準出力と標準エラーをタプルで返します。PIPEを指定しなかったほうはNoneになります。

stderrをstdoutにまとめる

import subprocess

p = subprocess.Popen(["ls", "/no/such/dir"], stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT, text=True)
out, err = p.communicate()
print("out:", out.strip())
print("err:", err)

実行結果:

out: ls: /no/such/dir: No such file or directory
err: None

ログに出る順番どおりにまとめて読みたいときはstderr=subprocess.STDOUTを使います。

communicate()は1回しか呼べない

同じプロセスに2回communicate()を呼ぶと、1回目でストリームが閉じているためValueError: I/O operation on closed file.になります。結果は1回目で変数に入れておいてください。

stdoutを1行ずつリアルタイムに読む

Popenを使う大きな理由の1つです。p.stdoutをforで回すと、コマンドが出力した行から順に受け取れます。

import subprocess

cmd = ["sh", "-c", "for i in 0 1 2; do echo step $i; sleep 0.2; done"]
with subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) as p:
    for line in p.stdout:
        print("受信:", line.rstrip())
print("returncode:", p.returncode)

実行結果(0.2秒ごとに1行ずつ表示されます):

受信: step 0
受信: step 1
受信: step 2
returncode: 0

長時間動くコマンドの進捗を表示したいときや、巨大な出力を一度にメモリへ載せたくないときに使います。run()では、終わるまで何も受け取れません。

なお、呼び出す側がPythonスクリプトの場合、出力がバッファにたまってまとめて届くことがあります。そのときは呼び出すコマンドをpython3 -uで起動するか、子のスクリプトでprint(..., flush=True)にしてください。

標準入力にデータを渡す

import subprocess

p = subprocess.Popen(["wc", "-l"], stdin=subprocess.PIPE,
                     stdout=subprocess.PIPE, text=True)
out, _ = p.communicate("x\ny\nz\n")
print("行数:", out.strip())

実行結果:

行数: 3

stdin=subprocess.PIPEを指定し、communicate()の引数に渡したい文字列を入れます。

wait()とPIPEの組み合わせはデッドロックする

公式ドキュメントが警告している落とし穴です。stdout=PIPEを指定したままwait()を先に呼ぶと、出力の多いコマンドで固まることがあります。

p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
p.wait()              # 出力が多いと固まる
out = p.stdout.read()

OSのパイプには容量の上限があり、いっぱいになるとコマンド側は「読まれるのを待つ」状態になります。Python側は「終わるのを待つ」ので、お互いに待ち続けます。PIPEを使うときはwait()ではなくcommunicate()を使うよう、ドキュメントでも案内されています。

timeoutを指定する

import subprocess

p = subprocess.Popen(["sleep", "5"], stdout=subprocess.PIPE, text=True)
try:
    out, _ = p.communicate(timeout=0.5)
except subprocess.TimeoutExpired:
    p.kill()
    out, _ = p.communicate()
    print("タイムアウトしたので停止。returncode:", p.returncode)

実行結果:

タイムアウトしたので停止。returncode: -9

communicate(timeout=秒)は時間切れでTimeoutExpiredを出しますが、プロセス自体は止まりません。kill()で止めたあと、もう一度communicate()を呼んで後片付けをするのが公式ドキュメントの書き方です。

プロセスを止める:terminate()とkill()

import subprocess

p = subprocess.Popen(["sleep", "10"])
p.terminate()
p.wait()
print("terminate:", p.returncode)

p = subprocess.Popen(["sleep", "10"])
p.kill()
p.wait()
print("kill:", p.returncode)

実行結果:

terminate: -15
kill: -9

macOS・Linuxでは、シグナルで止まったプロセスのreturncodeはマイナスのシグナル番号になります。terminate()はSIGTERM(15)で終了を求め、kill()はSIGKILL(9)で強制終了します。まずterminate()、止まらなければkill()の順が基本です。

複数のコマンドを同時に動かす

import subprocess, time

t = time.perf_counter()
ps = [subprocess.Popen(["sleep", "0.3"]) for _ in range(3)]
for p in ps:
    p.wait()
print(f"Popenで3つ同時: {time.perf_counter() - t:.2f}秒")

t = time.perf_counter()
for _ in range(3):
    subprocess.run(["sleep", "0.3"])
print(f"run()で3つ順番: {time.perf_counter() - t:.2f}秒")

実行結果:

Popenで3つ同時: 0.31秒
run()で3つ順番: 0.93秒

先に全部起動してから、まとめてwait()するのが並行実行の基本形です。待ち時間の長いコマンド(変換処理や通信)をいくつも動かすときに効きます。

パイプでつなぐ(コマンド | コマンド)

import subprocess

p1 = subprocess.Popen(["printf", "b\na\nb\n"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["sort", "-u"], stdin=p1.stdout,
                      stdout=subprocess.PIPE, text=True)
p1.stdout.close()
print(repr(p2.communicate()[0]))

実行結果:

'a\nb\n'

シェルのprintf ... | sort -uと同じです。p1.stdout.close()は、p2が先に終わったときにp1が終了を知るために必要で、公式ドキュメントの例にも入っています。

shell=Trueの使い方と注意点

import subprocess

p = subprocess.Popen("echo hello world | wc -w", shell=True,
                     stdout=subprocess.PIPE, text=True)
print("単語数:", p.communicate()[0].strip())

実行結果:

単語数: 2

shell=Trueにすると、コマンドを1つの文字列で渡し、パイプ(|)や環境変数をシェルに解釈させられます。手軽ですが、ユーザー入力など外から来た文字列を混ぜると、意図しないコマンドを実行される危険があります。公式ドキュメントの「セキュリティで考慮すべき点」でも注意されています。外部の値を使うときはリストで渡し、shell=Trueは避けてください。

with文で確実に後片付けする

Popenはwith文に対応しており、ブロックを抜けると標準入出力が閉じられ、プロセスの終了を待ちます。上の「1行ずつ読む」例のように、Popenを直接使うときはwithを習慣にすると閉じ忘れ・待ち忘れを防げます。

os.popen()との違い

import os, subprocess

print(repr(os.popen("echo hello").read()))
print(repr(subprocess.run(["echo", "hello"], capture_output=True, text=True).stdout))

実行結果:

'hello\n'
'hello\n'

「popen python」で検索するとos.popen()も出てきます。os.popen()はシェル経由で実行して出力を読むだけの簡単な関数で、終了コードの扱いやstderrの分離などはできません。公式ドキュメントにも、os.popen()をsubprocessに置き換える書き方の節があります。新しく書くコードではsubprocessを使ってください。

よくある質問

Q. subprocess.Popenとrun()はどちらを使えばよいですか?

終わるまで待って結果を受け取るだけならrun()を使います。待たずに別の処理を続けたい、出力を1行ずつ読みたい、途中で止めたい、複数を同時に動かしたいときにPopenを使います。

Q. poll()がNoneを返すのはなぜですか?

プロセスがまだ実行中だからです。終了すると終了コード(正常終了なら0)を返します。判定はif p.poll() is None:のように書きます。

Q. stdoutをリアルタイムに取得するには?

stdout=subprocess.PIPEとtext=Trueを指定して起動し、for line in p.stdout:で1行ずつ読みます。子がPythonなら-uオプションかflush=Trueで出力のため込みを防ぎます。

Q. wait()で固まってしまうのはなぜですか?

stdout=PIPEを指定したまま出力が多いと、パイプがいっぱいになって互いに待ち続けるためです。PIPEを使うときはwait()ではなくcommunicate()を使います。

まとめ

  • Popenは起動後すぐに戻る。poll()は実行中ならNone、wait()で終了を待つ
  • 出力はstdout=PIPE・stderr=PIPEとcommunicate()で受け取る(1回だけ)
  • for line in p.stdoutでリアルタイムに1行ずつ読める
  • PIPEとwait()の組み合わせはデッドロックの原因。communicate()を使う
  • timeoutのあとはkill()してから再度communicate()
  • 外部の文字列を使うときはshell=Trueを避ける
  • 結果をまとめて受け取るだけならrun()で十分

あわせて読みたい

対面・無料|東京・永田町で毎週土曜日13時

1回の参加で、プログラミングの構造がわかる

ゼロからはじめるPython入門講座

8年間・累計600回開催/これまで全国で2万人以上が参加

「やはり対面で聞けるのはとても気づきがあり、価値があると思います」

参加者の声

  • 本当に初心者からの講座です。プログラミングに触ったことがなくて大丈夫です
  • 対面だから、その場で聞けます。つまずいた所を持ち帰らずに解決できます
  • 講座は1時間。そのあとも残って学習を続けられ、講師にも質問できます
  • 独学で挫折した方も。糸口が見つかって、講座のあと会員になる方が多くいます

無料で参加する|開催日程を見る

参加費は無料です