目次
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
コマンドによって作成できます。無名パイプについて、たとえば、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
前述のとおり、dd
と zstd
をつなぐとき、シェルでは、|
という記号で表現されるパイプを使います。一方、Pythonの subprocess
において、パイプは subprocess.PIPE
によって表現されます。
そこで、Popenインターフェースの出番です。次の節では、本節ではPopenについてもう少し深く掘り下げます。
Popenを使う
Popenは、公式ドキュメントによれば、subprocess.run()
といった簡易関数によってカバーされないような、あまり一般的でないケースを開発者が扱えるように、多くの柔軟性を提供するオブジェクトです。
本節ではまず最初に、ざっくりとその使い方に触れます。そして、前節の課題であった、2つ以上のプロセスをパイプでつなぎ、ddの出力をPython側でキャプチャする実演を行います。そして最後に、Pythonから直接プログラムにデータを送信する例についても触れていきます。
プロセスが一つのみ
それでは、プロセスが1つだけの場合における標準出力、および標準エラー出力のキャプチャ方法を見ていきましょう。簡単そうに見えますが、Popenを利用する場合は少し手順が増えます。
まずは、Popen
クラスを、引数とstdout=PIPE
, stderr=PIPE
をともに指定してインスタンス化します。stdout, stderr
を指定した理由は、プロセスの標準出力と標準エラー出力を捕捉し、Python内で利用できるようにするためです。次に、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
のようにして取り出せます。
>>> 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にターミナル画面を荒らされました
>>>
標準エラー出力が不要な場合は/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つ以上のプロセス
それでは、先ほどのシェルコマンドdd if=/dev/urandom bs=1k count=1 | wc
に相当する操作をPythonで行う方法を見ていきます。最終的には、このコマンドの標準出力と標準エラー出力を取得し、Python内で利用できることを目標にします。
まずは、2つのプロセスをPopenを使って生成します。1つ目のプロセスoneの標準出力は、そのままパイプに渡します。標準エラー出力は不要なため、DEVNULLを指定しています。
>>> from subprocess import Popen, PIPE, DEVNULL
>>> one = Popen(['dd', 'if=/dev/zero', 'bs=1', 'count=1'], stdout=PIPE, stderr=DEVNULL)
ここがミソなのですが、2つめのプロセスの標準入力は、一つ目のプロセスである one
の標準出力に設定しておくようにします。あとは全て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()
''
Pythonから直接プロセスに接続
もちろん、Python側で保持していたデータを別のプログラムにパイプを通じて流すこともできます。今回は、5行にわたって’Hello’とつぶやく文字列を、wc
コマンドに渡して、その改行数を結果として捕捉する以下のプログラムを想定します。
本プログラムでは、1番目のプロセスの標準入力をパイプに設定しています。そして、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について紹介しました。ユースケースはごく一部でしたが、役に立てば幸いです。
関連投稿
Pythonで対話的UIを実装したいときはreadlineモジュールを使おう
Pythonのreadlineモジュールを使って、インラインで修正ができる対話的UIを実装します。 続きを読む
コメント
コメントはありません。