目次

subprocessのPopenを使ってみよう
Pythonのsubprocessは便利なモジュールです。subprocess.run()のように簡単に使えるものから、Popenのようなインターフェースを用いて、パイプを使って柔軟にコマンドを組み合わせて結果を取得したり、処理をその処理系に委任したりすることもできます。そうした当モジュールですが、本記事では特にPopenについて、3つほどのありそうな使用例を例を交えて紹介してみようと思います。
検証環境
uname -srvmpio
# Linux 5.15.133.1-microsoft-standard-WSL2 #1 SMP Thu Oct 5 21:02:42 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
python3 --version
# Python 3.10.12
subprocess.run()の使い道は限られる
subprocess.run()
は優秀で、簡単に子プロセスを立ち上げ、その実行結果を取得できます。たとえば、以下のコマンドは、dd
にブロックデバイスの/dev/zero
から10バイトを1回分コピーして来い!という命令です。実際、バイト数を数えることができる関数である、len()
で確かめると、確かに10バイトのデータがコピーされていることが分かります。
>>> import subprocess
>>> process = subprocess.run(['dd', 'if=/dev/zero', 'bs=10', 'count=1'], capture_output=True)
>>> process
CompletedProcess(args=['dd', 'if=/dev/zero', 'bs=10', 'count=1'], returncode=0, stdout=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', stderr=b'1+0 records in\n1+0 records out\n10 bytes copied, 6.6194e-05 s, 151 kB/s\n')
>>> len(process.stdout)
10
subprocessでもパイプを使いたい!
しかし、subprocess.run()で対応できない場合もあります。たとえば、2つ以上のコマンドをパイプを使って組み合わせて、何か有意な結果を得たいという場合です。
パイプ
パイプは、2つの異なるプロセスがデータをやり取りするときに用いるデータの通り道であり、プロセス間通信(IPC)の一つです。パイプは、FIFO(First in First Out)と呼ばれるデータ構造に基づいていて、一種のキューです。Linuxを含め各種UNIX系のOSにおいては、無名パイプ(Anonymous Pipe)と名前付きパイプ(Named Pipe)というものがあり、前者は、シェルの |
、後者は、mkfifo
コマンドによって作成できます。
課題: シェルでパイプを使う処理をどのようにPythonで書くか?
たとえば、Bourne Shell や Bash Shellで、wc
に dd
が出力した1KBのデータを送信したいときは、以下のように通信させたいコマンドを|
でつなぎます。この際、無名パイプが作成され、パイプを通じてデータが送られます。
dd if=/dev/urandom bs=1k count=1 | wc
# 1+0 records in
# 1+0 records out
# 1024 bytes (1.0 kB, 1.0 KiB) copied, 6.6427e-05 s, 15.4 MB/s
# 4 19 1024
さて、これと同じようなことをPythonでするには、Popenインターフェースを使用しなければなりません。次の節では、Popenについてもう少し深く掘り下げます。
Popenを使う3つの実例
Popenは、公式ドキュメントによれば、subprocess.run()
といった簡易関数によってカバーされないような、あまり一般的でないケースを開発者が扱えるように、多くの柔軟性を提供するオブジェクトです。
本節ではまず最初に、ざっくりとその使い方に触れます。そして、前節の課題であった、2つ以上のプロセスをパイプでつなぎ、ddの出力をPython側でキャプチャする実演を行います。そして最後に、Pythonから直接プログラムにデータを送信する例についても触れていきます。
実例1. プロセスが一つのみ
それでは、プロセスが1つだけの場合における標準出力、および標準エラー出力のキャプチャ方法を見ていきましょう。Popenを利用する場合は少し手順が増えます。
Pythonの subprocess
において、プロセスはPopenで表現されます。シェルの場合では、パイプは|
に相当していましたが、subprocessにおけるパイプ(PIPE)はプロセスの標準出力と標準エラー出力を捕捉し、Python内で利用できるようにするための出力先です。模式化するならこんな感じです。
dd if=/dev/urandom bs=1k count=1 --> <Pythonインタプリタ>
PIPEオブジェクト ^^^
このPIPEを、Popen
クラスの引数とstdout
, stderr
のそれぞれに指定してインスタンス化します。次に、communicate()を呼び出し、プロセスを実行します。
>>> from subprocess import Popen, PIPE
>>> proc = Popen(['dd', 'if=/dev/zero', 'bs=10', 'count=1'], stdout=PIPE, stderr=PIPE)
>>> outs, errs = proc.communicate()
communicate()
は、標準入力から、proc
の標準出力(と標準エラー出力)を、End-of-fileに達するまで読み出したあと、proc
が終了し、returncode属性を付けるのを確認するまで待ってから実行結果のタプルを返します。
outs, errsの中身を見ると、前段でstdout
, stderr
の出力先をPIPEに設定していたため、プロセスprocの標準出力と標準エラー出力を、それぞれ取り出せていることがわかります。
>>> outs
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> errs
b'1+0 records in\n1+0 records out\n10 bytes copied, 1.7363e-05 s, 576 kB/s\n'
なお、Popenコンストラクタで、stdout
、あるいは stderr
を指定しない場合、これらのデフォルト値はNone
になります。None
の場合、プロセスの出力は現在のシェルに出力されます。
>>> proc = Popen(['dd', 'if=/dev/zero', 'bs=10', 'count=1'])
>>> 1+0 records in
1+0 records out
10 bytes copied, 8.1815e-05 s, 122 kB/s
# Pythonにターミナル画面を荒らされました
>>>
標準入力だけ欲しくて、標準エラー出力は不要な場合はstderr=に/dev/null
に相当するDEVNULL
を指定すると良いでしょう。
>>> from subprocess import Popen, PIPE, DEVNULL
>>> proc = Popen(['dd', 'if=/dev/zero', 'bs=10', 'count=1'], stdout=PIPE, stderr=DEVNULL)
>>> outs, errs = proc.communicate()
>>> outs
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> errs
>>>
実例2. 2つ以上のプロセスを通信させる
次に、先ほどのシェルコマンドdd if=/dev/urandom bs=1k count=1 | wc
に相当する操作をPythonで行い、wcの標準出力と標準エラー出力を取得し、Python内で利用する方法を見ていきましょう。
まずは、2つのプロセスをPopenを使って生成します。1つ目のプロセスoneの標準出力は、そのままPIPEに渡します。標準エラー出力は不要なため、DEVNULLを指定しています。
>>> from subprocess import Popen, PIPE, DEVNULL
>>> one = Popen(['dd', 'if=/dev/zero', 'bs=1', 'count=1'], stdout=PIPE, stderr=DEVNULL)
上で、oneの標準出力先をPIPEに設定したため、oneの標準出力がone.stdoutとして利用できます。twoのプロセスの処理内容は、oneの標準出力に依存するため、twoの標準入力を、one.stdoutに設定しておくようにします。あとは全てPIPEにつなぎます。
>>> two = Popen(['wc'], stdin=one.stdout, stdout=PIPE, stderr=PIPE)
最後に、twoに生えているcommunicate()を呼び出すことで、プロセス間の通信を開始します。このようにして、先ほどと同じように実行結果を確認できます。
>>> outs, errs = two.communicate(timeout=15)
>>> outs.decode()
' 0 0 1\n'
>>> errs.decode()
''
実例3. Pythonから直接プロセスに接続する
もちろん、Python側で保持していたデータを別のプログラムにPIPEを通じて流すこともできます。今回は、5行にわたって’Hello’とつぶやく文字列を、wc
コマンドに渡して、その改行数を結果として捕捉する以下のプログラムを想定します。
本プログラムでは、1番目のプロセスの標準入力をPIPEに設定しています。そして、communicate()の引数にはinput=を指定し、直接プロセスに文字列(.encode()しているのでバイト列です)を送信していることに注目しましょう。
>>> from subprocess import Popen, PIPE
>>> txt = "".join(["Hello\n" for _ in range(5)])
>>> wc = Popen(['wc', '-l'], stdin=PIPE, stdout=PIPE)
>>> outs, errs = wc.communicate(input=txt.encode(), timeout=15)
>>> outs.decode()
'5\n'
まとめ
本記事では、Pythonを含む2つ以上のアプリケーションでデータをやり取りさせたい場合に便利なPopenについて紹介しました。subprocess.PIPEの概念は少し難しいですが、自分で色々遊んでみると使っているうちに理解できてくると思います。
ユースケースはごく一部でしたが、この記事が役に立てば幸いです。
関連投稿

Pythonで対話的UIを実装したいときはreadlineモジュールを使おう
Pythonのreadlineモジュールを使って、インラインで修正ができる対話的UIを実装します。 続きを読む
コメント
コメントはありません。