LoginSignup
181
141

More than 3 years have passed since last update.

subprocessについてより深く(3系,更新版)

Last updated at Posted at 2019-12-05

はじめに

2017年に書いた記事の内容が2系ベースであり,かついい加減情報を更新したほうがいいなと思い,編集に着手した結果,subprocess.run()をはじめとする大幅な追記が必要となりそうになったため,本記事を新規に作成した.

目標として,以前からのsubprocess関数の説明もしつつ(サポートは終了していない),subprocess.run()subprocess.Popen()による同義な記述を行う.さらには,これらを用いたより多様な記述を取り上げる.

お急ぎの方はcmd記述の共通ルールと,run()以降を読めば全く問題ない.

そもそも何をするモジュールなんですか,という話は公式か,この記事などを参照するとよい.

結論は何かと言われれば,今後はできる範囲ではすべてsubprocess.run()に任せよう.それより複雑な処理が求められる場合は,subprocess.run()の土台でもあるsubprocess.Popen()を使用しよう.調べた限りは本記事で他に紹介する関数は一切不要(すべて代替可能)だよ,ということ.

使用モジュール
import io
import os
import time
import subprocess

実験用ファイル

hello.py
print('Hello world!')

古いシェルコマンド実行手法

もはや使う機会のないものばかりだが,古いコード発掘時の助けになるやもなので一応まとめる.

os.system()

os.system(command)公式
サブシェル内でコマンド (文字列) を実行します。この関数は標準 C 関数 system() を使って実装されており、system() と同じ制限があります。sys.stdin などに対する変更を行っても、実行されるコマンドの環境には反映されません。
(中略)
subprocess モジュールは、新しいプロセスを実行して結果を取得するためのより強力な機能を提供しています。この関数の代わりに subprocess モジュールを利用することが推奨されています。

os.system()の例
# ターミナル上に結果が出力される
print(os.system('ls')) # 0

# ターミナル上→ sh: 1: tekito: not found
print(os.system('tekito')) # 32512(←環境依存? 0以外の数字が返る)

os.spawn関数群(os.spawnl(mode, path, ...)os.spawnle(mode, path, ..., env)os.spawnlp(mode, file, ...)os.spawnlpe(mode, file, ..., env)os.spawnv(mode, path, args)os.spawnve(mode, path, args, env)os.spawnvp(mode, file, args)os.spawnvpe(mode, file, args, env))はよくわからない詳しくやると面倒そうなので割愛(spawnという動詞自体は,「(プロセスを)生成する」といったニュアンスらしい).

os.popen()

os.popen(cmd, mode='r', buffering=-1)公式
コマンド cmd への、または cmd からのパイプ入出力を開きます。戻り値はパイプに接続されている開かれたファイルオブジェクトで、 mode が 'r' (デフォルト) または 'w' かによって読み出しまたは書き込みを行うことができます。
(中略)
これは、subprocess.Popen を使用して実装されています。サブプロセスを管理し、サブプロセスと通信を行うためのより強力な方法については、クラスのドキュメンテーションを参照してください。

os.popen()の例
# 出力結果をreadする
print(os.popen('ls h*.py','r').readlines())
'''
['hello.py\n', 'hello2.py\n']
'''

subprocessにおけるcmd記述の共通ルール

文字列で与える→shell=True
・ クオート混入による誤作動リスク
文字列のリストで与える→shell=False (default)
・ ワイルドカード等が使えず,低自由度

公式
shell が True なら、指定されたコマンドはシェルによって実行されます。あなたが Python を主として (ほとんどのシステムシェル以上の) 強化された制御フローのために使用していて、さらにシェルパイプ、ファイル名ワイルドカード、環境変数展開、~ のユーザーホームディレクトリへの展開のような他のシェル機能への簡単なアクセスを望むなら、これは有用かもしれません。
公式
shell=True でシェルを明示的に呼びだした場合、シェルインジェクション の脆弱性に対処するための、すべての空白やメタ文字の適切なクオートの保証はアプリケーションの責任になります。

shell=Trueでしか実現できないコマンド
# 文字列リストで与える方法(以降の5つはこの記法では不可)
print(subprocess.call(['ls','-l'], shell=False)) # 0

# シェルパイプライン
print(subprocess.call('echo -e "a\nb\nc" | wc -l', shell=True)) # 0
# セミコロン
print(subprocess.call('echo Failed.; exit 1', shell=True)) # 1
# ワイルドカード
print(subprocess.call('ls -l *.py', shell=True)) # 0
# 環境変数
print(subprocess.call('echo $HOME', shell=True)) # 0
# HOMEを表すチルダ記号
print(subprocess.call('ls -l ~', shell=True)) # 0

subprocess.run()による統合前の関数たち

下記に示す3種類がある.それらは下表の3要素で大別される.いずれもsubprocess.run()においてON/OFF切り替えが可能となったため,今後は一切不要となった.

関数名 終了ステータス 出力結果 CalledProcessError
(.run()での引数・attribute) .returncode .stdout check
subprocess.call()
subprocess.check_call()
subprocess.check_output()

subprocess.call()

subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None)
コマンドの実行.返り値は終了ステータス(0なら正常終了).

主要なオプション①(標準入出力,タイムアウト)

(他のsubprocess関数でも共通)

標準入力・出力にファイルを指定する際は,open()に入れた状態で.
余談に近いが,Python3系では,str型(mode='r'/'w')よりもbytes型(mode='rb'/'wb')を選んでおけばだいたい問題ない(調べればわかる話なのだが,どっちもサポートしているか,bytes型のみサポートしている場合が多い).

主要なオプション使用例
print(subprocess.call(['cat', 'hello.py'])) # 0
# 標準入力・標準出力を指定する(2つ目はhello2.pyが作成される)
print(subprocess.call(['cat'], stdin=open('hello.py','rb'))) # 0
print(subprocess.call(['cat', 'hello.py'], stdout=open('hello2.py','wb'))) # 0

# timeoutを指定する
print(subprocess.call(['sleep', '3'], timeout=1)) # TimeoutExpired

# (補足)実行ディレクトリ指定のcwdオプションでも~はサポートされていない.
print(subprocess.call(['ls','-l'], shell=False, cwd = "~")) # FileNotFoundError

subprocess.check_call()

subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None)
コマンドの実行.返り値は終了ステータス(0なら正常終了).異常終了時に,CalledProcessErrorを返す.

check_call()使用例
# 正常終了
print(subprocess.call(['cat', 'hello.py'])) # 0
print(subprocess.check_call(['cat', 'hello.py'])) # 0

# 異常終了:エラーステータスではなく,例外エラーを返す
print(subprocess.call(['cat', 'undefined.py'])) # 1
print(subprocess.check_call(['cat', 'undefined.py'])) # CalledProcessError

subprocess.check_output()

subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None, text=None)
コマンドの実行.返り値が標準出力.

公式
デフォルトで、この関数はデータをエンコードされたバイトとして返します。

ということなので,文字列などであればデコードしてから処理を行う.

check_output()使用例
o = subprocess.check_output('ls h*.py', shell=True)
print(o) # b'hello.py\nhello2.py\n'
print(o.decode().strip().split('\n')) # ['hello.py', 'hello2.py']
主要なオプション②(標準エラー出力,特殊値)

(他のsubprocess関数でも共通)

stderrに与えるものによって出力先を変更できる.

  • subprocess.DEVNULL:標準入出力先をos.devnull(ビットバケツ、ブラックホール)に指定
  • subprocess.PIPE:標準入出力先へのパイプ指定
  • subprocess.STDOUT:標準エラー出力が標準出力と同じハンドルに出力されるよう指定(2>1&.stderrに対してのみ)
標準エラー出力関連操作
# 出力はすべて捨てる
o = subprocess.call(['cat', 'undefined.py'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)

# 標準出力を取り出す
try:
    o = subprocess.check_output(['cat', 'hello.py'])
    print(o) # b"print('Hello world!')"
except subprocess.CalledProcessError as e:
    print('ERROR:', e.output)

# 標準エラー出力を取り出す
try:
    o = subprocess.check_output(['cat', 'undefined.py'], stderr=subprocess.PIPE)
    print(o)
except subprocess.CalledProcessError as e:
    print('ERROR:', e.stderr) # ERROR: b'cat: undefined.py: No such file or directory\n'

# 標準エラーを標準出力に統合して取得
try:
    o = subprocess.check_output(['cat', 'undefined.py'], stderr=subprocess.STDOUT)
    print(o)
except subprocess.CalledProcessError as e:
    # e.stderrでなくe.stdoutなのに注意.なお,e.outputでも可.
    print('ERROR:', e.stdout) # ERROR: b'cat: undefined.py: No such file or directory\n'

subprocess.run()による置換

公式
サブプロセスを起動するために推奨される方法は、すべての用法を扱える run() 関数を使用することです。より高度な用法では下層の Popen インターフェースを直接使用することもできます。

ということで,先述のコードたちのrun()による置換.指定オプションなどは上表も参照のこと.

subprocess.run()

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)

subprocess.call()の置換

.returncodeをつける.
subprocess.call()の紹介時に用いたstdin以外にも,inputというオプションがある.これは,後述するsubprocess.Popen().communicate()を用いた標準入力の指定であり,文字列を指定する.

run()置換:主要なオプション使用例
print(subprocess.run('ls -l h*.py', shell=True).returncode) # 0
# 標準入力・標準出力を指定する(hello2.pyが作成される)
print(subprocess.run(['cat'], stdin=open('hello.py','rb'), stdout=open('hello2.py','wb')).returncode) # 0
# (input使用.上とほぼ同義)
print(subprocess.run(['cat'], input=open('hello.py','rb').read(), stdout=open('hello2.py','wb')).returncode) # 0

# timeoutを指定する
print(subprocess.run(['sleep', '3'], timeout=1).returncode) # TimeoutExpired
subprocess.check_call()の置換

check=Trueを追加する.また,.check_returncode()という後出し用メソッドもある.

run()置換:check_call()使用例
# 成功ステータス
print(subprocess.run(['cat', 'hello.py']).returncode) # 0
print(subprocess.run(['cat', 'hello.py'], check=True).returncode) # 0

# エラーステータスではなく,例外エラーを返す
print(subprocess.run(['cat', 'undefined.py']).returncode) # 1
print(subprocess.run(['cat', 'undefined.py'], check=True).returncode) # CalledProcessError

# (補足)check_returncode()を用いた後出しエラー出力
p = subprocess.run(['cat', 'undefined.py'], check=False)
print(p.returncode) # 1
print(p.check_returncode()) # CalledProcessError
subprocess.check_output()の置換

stdoutオプションと.stdoutを使用.

run()置換:check_output()使用例
o = subprocess.run('ls h*.py', shell=True, stdout=subprocess.PIPE, check=True).stdout
print(o) # b'hello.py\nhello2.py\n'
print(o.decode().strip().split('\n')) # ['hello.py', 'hello2.py']

標準エラー出力関連操作も,新規のオプション機能により,run()を用いるとcheck_output()のときよりも自由度が広がる.

標準エラー出力関連操作
# check=False (default)で,エラーで止まることなくstrerrを覗ける
o = subprocess.run(['cat', 'undefined.py'], check=False, capture_output=True)
print((o.stdout, o.stderr)) # (b'', b'cat: undefined.py: No such file or directory\n')

# 標準エラー出力を標準出力に統合.
o = subprocess.run(['cat', 'undefined.py'], check=False, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
print(o.stdout) # b'cat: undefined.py: No such file or directory\n'

# capture_output=Trueで標準出力,標準エラー出力ともにPIPEが指定される.
try:
    o = subprocess.run(['cat', 'undefined.py'], check=True, capture_output=True)
    print(o.stdout)
except subprocess.CalledProcessError as e:
    print('ERROR:',e.stderr) # ERROR: b'cat: undefined.py: No such file or directory\n'

subprocess.Popen()による自由度の高い処理

公式
このモジュールの中で、根底のプロセス生成と管理は Popen クラスによって扱われます。簡易関数によってカバーされないあまり一般的でないケースを開発者が扱えるように、Popen クラスは多くの柔軟性を提供しています。

というわけで,柔軟性の紹介.特に,run()では再現できないものを中心に扱っていく.
大きな特徴として,subprocess.Popen()はプロセスを生成するだけで,終了を待たない.終了したかの確認には.poll()を使用し,終了を確認してから次に進むためには..wait()を実行する.

置換例(call()

Popen()の多くの引数やアトリビュートはrun()と共通である.
subprocess.call()の再現のためには,subprocess.run()でも使用した.returncodeを用いる.他のコードの再現時も,run()の時と同様にすればよい.

Popen()置換:call()使用例
# 標準入力・標準出力を指定する(2つ目はhello2.pyが作成される)
p = subprocess.Popen('ls -l h*.py', shell=True)
p.wait()
print(p.returncode) # 0

# 標準入力・標準出力を指定する(hello2.pyが作成される)
p = subprocess.Popen(['cat'], stdin=open('hello.py','rb'), stdout=open('hello2.py','wb'))
p.wait()
print(p.returncode) # 0

# timeoutを指定する
p = subprocess.Popen(['sleep', '3'])
p.wait(timeout=1) # TimeoutExpired

デッドロックとPopen.communicate()

Popen.communicate()は,(必要であれば文字列入力を入力を与えて),(標準出力, 標準エラー出力)の形のタプルを返すメソッド.

公式
プロセスと交信する:データを標準入力に送信します。 ファイルの終わりに達するまで、stdoutおよびstderrからデータを読み取ります。 プロセスが終了するのを待ちます。 (中略)communicate()はタプル (stdout_data, stderr_data) を返します。
公式
警告 .stdin.write, .stdout.read, .stderr.read を利用すると、別のパイプの OS パイプバッファーがいっぱいになってデッドロックが発生する恐れがあります。これを避けるためには communicate() を利用してください。

この記事でも詳細に解説がなされている.
バッファに入出力データがたまりすぎる状況が発生するリスクがあるなら,一度に管理してくれるcommunicate()を使いなさい,といった感じ.
実際にcommunicate()を用いてデッドロックに対応した方の(記事)も存在する.

デメリットとして,一つのプロセス内で複数回にわたる入力・出力のやり取りができなくなることがある.下記に紹介するリアルタイム出力管理やインタラクティブ処理の際はcommunicate()との併用ができない.

また,タイムアウト処理に必要なオプションtimeoutが,Popen().stroutなどにはなくてcommunicate()には存在するといった違いもある.これについては,この記事で紹介されているような解決策がある.

シェルパイプラインの再現

公式でも紹介されている.shell=Trueとして文字列でを与えてもよいが,下記のようにそれを避けた文法でも再現が可能.
途中のSIGPIPEについての詳細は,こことかこことかで説明がされている.

Popen()用例:シェルパイプライン
p1 = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE) # 出力先にパイプ
p2 = subprocess.Popen(['grep', 'python'], stdin=p1.stdout, stdout=subprocess.PIPE) # 入力にp1の受取
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
print(p2.communicate()[0].decode().strip().split('\n')) # stdout取得

.poll()による状態確認とリアルタイム出力管理

Popen()は終了を待たないという話をしたが,.poll()により,終了状態か否かを確認できる.終了していなかったらNone,終了していたらそのステータスを返す.
.stdout.readline()で1行ずつ出力を取得できる.これは,出力がリアルタイムで改行されるたびに取得が可能となる.対して.stdout.read()などの一度にすべての出力を取得するメソッドを用いると,自動で終了待機状態になる.

Popen()用例:リアルタイム出力管理
cmd = 'echo Start;sleep 0.5;echo 1;sleep 0.5;echo 2;sleep 0.5;echo 3;sleep 0.5;echo Finished'
p = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
# リアルタイムに取得
while p.poll() is None:
    print('status:',p.poll(), p.stdout.readline().decode().strip())
print('status:',p.poll())
print()

p = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
# 2秒たたないと出力されない
print('status:',p.poll(), p.stdout.read().decode().strip())
'''
status: None Start
status: None 1
status: None 2
status: None 3
status: None Finished
status: 0

status: None Start
1
2
3
Finished
'''

もっとちゃんとしたリアルタイム出力管理コードのひな型がこの記事で紹介されている.

インタラクティブ入出力

何回かに分けて入力を送り,その都度出力を得る,といったやり取りは,下記のようにして実現される.参考にしたのはこの記事
一度にデータの入出力を終えるcommunicate()では再現できないため,大きなデータのやり取りが必要になると,デッドロックが発生するリスクがある.

まず,準備として,複数の入力を必要とし,その都度出力を生成するpythonコードを作成しておく.

(準備)calc.py
a = int(input())
print('n    =', a)
b = int(input())
print(' + {} ='.format(b), a+b)
c = int(input())
print(' - {} ='.format(c), a-c)
d = int(input())
print(' * {} ='.format(d), a*d)
e = int(input())
print(' / {} ='.format(e), a/e)

これを,下記のように実行.flushしないとバッファにたまった状態で入力されていない状態のままになってしまっているので注意.

Popen()用例:インタラクティブ入出力
numbers = range(1, 6)

# 非インタラクティブ(communicate()を用いて一度にすべて送信)
p = subprocess.Popen(['python', 'calc.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 改行コードで連結
str_nums = '\n'.join(map(str, numbers))
o, e = p.communicate(input=str_nums.encode())
print(o.decode())

'''
n    = 1
 + 2 = 3
 - 3 = -2
 * 4 = 4
 / 5 = 0.2
'''

# インタラクティブ(.stdin.write()を駆使して一行ずつ送信)
p = subprocess.Popen(['python', 'calc.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for n in numbers:
    # 数字を送る
    p.stdin.write(str(n).encode())
    # 改行を送る(input()に対しての入力なので必須)
    p.stdin.write('\n'.encode())
    # バッファの解放(大事)
    p.stdin.flush()
    # 結果を一行読む(複数行にわたる場合はwhile文を使う)
    print(p.stdout.readline().decode().strip())

'''
n    = 1
 + 2 = 3
 - 3 = -2
 * 4 = 4
 / 5 = 0.2
'''

並列実行(情報不足)

Popen()は終了を待たないため,複数プロセスを同時に走らせることが可能.非同期処理といえばasyncioモジュールで管理がされるが,その中にsubprocessに対応させたものも存在する(公式).その他にも,この記事この記事など,非同期処理については様々な記事が存在する.様々な手法のなかでの取捨選択時には「何が最も低遅延で,かつリスクが少ないか」を把握しなければならないと思うが,現状そこまでの知識がないため,ここではこれ以上の議論は割愛する.
下記はとりあえずの簡易版.

並列実行
# 順列
print('start')
s = time.time()
running_procs = [
    subprocess.run(['sleep', '3']),
    subprocess.run(['sleep', '2']),
    subprocess.run(['sleep', '1']),
    ]
# run()だと順に実行を待って計6秒近くかかる
print('finish [run()]', time.time() - s)
# ----------------------------
# 並列
print('start')
s = time.time()
procs = [
    subprocess.Popen(['sleep', '3']),
    subprocess.Popen(['sleep', '2']),
    subprocess.Popen(['sleep', '1']),
    ]
# Popen()だと,開始だけならすぐに終わる
print('finish [run Popen()]', time.time() - s)

# 各プロセスの終了待ち
[p.wait() for p in procs]
# 一番長い時間のかかるプロセスに等しい約3秒で処理が完了する
print('finish [wait Popen()]', time.time() - s)

終わりに

run()を用いれば大抵の多種多様な処理がすぐに実行でき,Popen()を駆使すれば,非同期かつインタラクティブなはるかに自由度の広い応用が可能となることが分かる.

181
141
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
181
141