はじめに
Pythonでシステム開発や自動化スクリプトを作成している方、subprocessモジュールを使って外部プロセスを実行している方は多いのではないでしょうか。
subprocessモジュールは非常に便利で強力なツールですが、標準出力の扱いに悩んだことはありませんか?
私も以前、subprocessを使ったスクリプトを作成していた際、標準出力が大量に出力されてログが見づらくなったり、リソースを無駄に消費したりしてしまった経験があります。
そこで今回は、Pythonのsubprocessモジュールを使用する際に、標準出力を出力しないためのベストプラクティスを6つ紹介したいと思います。
コードサンプルを交えながら、初心者の方にもわかりやすく解説していきますので、ぜひ最後までお付き合いください。
○subprocessの標準出力とは
subprocessモジュールを使って外部プロセスを実行すると、そのプロセスが標準出力に出力した内容がPythonのプログラムにも表示されます。
これが「標準出力」です。
例えば、次のようなコードを実行すると、lsコマンドの実行結果が標準出力に表示されます。
実行結果
このように、subprocessを使って外部プロセスを実行すると、その標準出力がPythonのプログラムにも表示されてしまうのです。
○なぜ標準出力を制御する必要があるのか
では、なぜ標準出力を制御する必要があるのでしょうか。
主に次の2つの理由が挙げられます。
- 不要な出力によるログの混乱を避けるため
- リソースを効率的に使用し、パフォーマンスを最適化するため
1つ目の理由は、先ほどの例のように、外部プロセスの標準出力がそのままPythonのプログラムに表示されてしまうと、本来出力したいログが埋もれてしまい、ログが見づらくなってしまうことです。
特に、大量の標準出力が生成される場合は深刻です。
2つ目の理由は、標準出力を出力するためにはリソースを消費するため、パフォーマンスに影響を与えてしまうことです。
特に、頻繁に外部プロセスを実行するようなプログラムでは、標準出力の制御は重要になってきます。
●subprocess.run()で標準出力を出力しない方法
それでは、subprocess.run()を使って外部プロセスを実行する際に、標準出力を出力しない方法を見ていきましょう。
subprocess.run()は、Python 3.5以降で導入された比較的新しい関数で、外部プロセスの実行を簡単に行うことができます。
従来のsubprocess.call()やsubprocess.check_call()の代わりに使用することが推奨されています。
subprocess.run()を使って標準出力を出力しないようにするには、次の2つの方法があります。
- stdout=subprocess.DEVNULLを指定する
- capture_output=Trueを指定する
それぞれ見ていきましょう。
○サンプルコード1:stdout=subprocess.DEVNULL
1つ目の方法は、subprocess.run()の引数にstdout=subprocess.DEVNULLを指定する方法です。
subprocess.DEVNULLは、Unixシステムにおける/dev/nullと同様の機能を持つPythonの特殊ファイルです。
/dev/nullは、「ビットバケット」とも呼ばれ、書き込まれたデータを全て破棄します。
つまり、標準出力をsubprocess.DEVNULLにリダイレクトすることで、出力を抑制することができるのです。
ここでは、lsコマンドを実行し、その標準出力を抑制する例を紹介します。
実行結果
stdout=subprocess.DEVNULLを指定したことで、lsコマンドの実行結果が標準出力に出力されないことがわかります。
ちなみに、stderr=subprocess.DEVNULLを指定すれば、標準エラー出力も同様に抑制することができます。
実行結果
存在しないファイルを指定してlsコマンドを実行しているため、本来ならば標準エラー出力にエラーメッセージが表示されるはずですが、stderr=subprocess.DEVNULLを指定したことで、エラーメッセージも抑制されています。
このように、subprocess.DEVNULLを使えば、簡単に標準出力と標準エラー出力を抑制することができます。
○サンプルコード2:capture_output=True
2つ目の方法は、subprocess.run()の引数にcapture_output=Trueを指定する方法です。
capture_output=Trueを指定すると、標準出力と標準エラー出力がsubprocess.CompletedProcessオブジェクトのstdoutとstderrアトリビュートにキャプチャされます。
つまり、標準出力と標準エラー出力がプログラム内で利用可能になるのです。
ここでは、lsコマンドを実行し、その標準出力をキャプチャする例を紹介します。
実行結果
capture_output=Trueを指定したことで、lsコマンドの実行結果がresult.stdoutにキャプチャされ、print()で出力することができました。
ここで、text=Trueを指定していることに注目してください。
text=Trueを指定すると、stdoutとstderrがバイト列ではなく文字列として返されます。
text=Trueを指定しない場合は、stdoutとstderrをデコードする必要があります。
実行結果
result.stdout.decode()とすることで、バイト列を文字列にデコードしています。
capture_output=Trueを指定すれば、標準出力と標準エラー出力をプログラム内で利用することができます。
これは、外部プロセスの実行結果を解析したい場合などに便利です。
ただし、capture_output=Trueを指定しても、標準出力と標準エラー出力がターミナルに表示されてしまうことがあります。
その場合は、stdout=subprocess.PIPEとstderr=subprocess.PIPEを併用しましょう。
実行結果
stdout=subprocess.PIPEとstderr=subprocess.PIPEを指定することで、標準出力と標準エラー出力がパイプを通じてプログラムにリダイレクトされ、ターミナルには表示されなくなります。
●subprocess.Popen()で標準出力を出力しない方法
次は、subprocess.Popen()を使って外部プロセスを実行する際に、標準出力を出力しない方法を見ていきましょう。
subprocess.Popen()は、subprocess.run()と比べてより低レベルなインターフェースを提供しており、外部プロセスの実行をより細かく制御することができます。
subprocess.Popen()を使って標準出力を出力しないようにするには、次の2つの方法があります。
- stdout=subprocess.PIPEを指定する
- stdout=subprocess.DEVNULLを指定する
それぞれ見ていきましょう。
○サンプルコード3:stdout=subprocess.PIPE
1つ目の方法は、subprocess.Popen()のstdout引数にsubprocess.PIPEを指定する方法です。
subprocess.PIPEは、外部プロセスの標準出力をパイプを通じてプログラムに渡すための特殊値です。
stdout=subprocess.PIPEを指定することで、外部プロセスの標準出力をプログラム内で読み取ることができます。
ここでは、lsコマンドを実行し、その標準出力をプログラム内で読み取る例を紹介します。
実行結果
まず、subprocess.Popen()でlsコマンドを実行し、stdout=subprocess.PIPEを指定することで、標準出力をパイプに接続しています。
text=Trueを指定しているので、パイプから読み取るデータは文字列として扱われます。
次に、proc.communicate()を呼び出すことで、外部プロセスの実行が終了するまで待ち、パイプから標準出力と標準エラー出力を読み取ります。
ここでは、標準出力だけを読み取っているので、proc.communicate()[0]で標準出力を取得しています。
最後に、取得した標準出力をprint()で出力しています。
このように、stdout=subprocess.PIPEを指定することで、標準出力をプログラム内で読み取ることができます。
ただし、標準出力がターミナルに表示されることはありません。
○サンプルコード4:stdout=subprocess.DEVNULL
2つ目の方法は、subprocess.Popen()のstdout引数にsubprocess.DEVNULLを指定する方法です。
これは、先ほどのsubprocess.run()の場合と同様です。
subprocess.DEVNULLを指定することで、標準出力を/dev/nullにリダイレクトし、出力を抑制することができます。
ここでは、lsコマンドを実行し、その標準出力を抑制する例を見ていきましょう。
実行結果
subprocess.Popen()でlsコマンドを実行し、stdout=subprocess.DEVNULLを指定することで、標準出力を/dev/nullにリダイレクトしています。
proc.wait()を呼び出すことで、外部プロセスの実行が終了するまで待ちます。
標準出力が/dev/nullにリダイレクトされているため、何も出力されません。
●標準出力のリダイレクト
ここまで、subprocess.run()とsubprocess.Popen()を使って標準出力を出力しない方法を見てきましたが、標準出力を別のファイルにリダイレクトすることもできます。
標準出力をファイルにリダイレクトすることで、外部プロセスの実行結果を後から確認したり、ログとして保存したりすることができます。
○サンプルコード5:stdoutをファイルにリダイレクト
ここでは、lsコマンドを実行し、その標準出力をファイルにリダイレクトする例を紹介します。
実行結果
ファイルの内容(output.txt)
まず、open()関数を使って、出力先のファイル(output.txt)をwモード(書き込みモード)で開きます。
with文を使っているので、ファイルを閉じる処理は自動的に行われます。
次に、subprocess.Popen()でlsコマンドを実行しますが、ここでstdout引数にファイルオブジェクト(f)を指定しています。
text=Trueを指定しているので、ファイルには文字列として書き込まれます。
proc.wait()を呼び出して、外部プロセスの実行が終了するまで待ちます。
実行結果を見ると、標準出力は何も出力されていません。
代わりに、output.txtファイルにlsコマンドの実行結果が書き込まれています。
このように、標準出力をファイルにリダイレクトすることで、外部プロセスの実行結果を保存することができます。
ただし、ファイルにリダイレクトする場合は、ファイルのパーミッションに注意が必要です。
書き込み権限がない場合はエラーになります。
また、ファイルがすでに存在する場合は、上書きされてしまうので注意しましょう。
必要に応じて、ファイルをaモード(追記モード)で開くことで、ファイルの末尾に追記することもできます。
以上のように、標準出力をファイルにリダイレクトすることで、外部プロセスの実行結果を保存することができます。
●エラー処理
外部プロセスを実行する際は、エラーが発生する可能性があります。
エラーが発生した場合は、適切に処理する必要があります。
○サンプルコード6:stderr=subprocess.PIPE
ここでは、存在しないコマンドを実行し、標準エラー出力をプログラム内で読み取る例を紹介します。
実行結果
ここでは、subprocess.Popen()で存在しないコマンド(nonexistent_command)を実行しています。
stderr引数にsubprocess.PIPEを指定することで、標準エラー出力をパイプに接続しています。
proc.communicate()を呼び出すことで、外部プロセスの実行が終了するまで待ち、パイプから標準出力と標準エラー出力を読み取ります。
ここでは、標準エラー出力だけを読み取っているので、proc.communicate()[1]で標準エラー出力を取得しています。
最後に、取得した標準エラー出力をprint()で出力しています。
存在しないコマンドを実行したので、標準エラー出力にエラーメッセージが表示されています。
このように、stderr=subprocess.PIPEを指定することで、標準エラー出力をプログラム内で読み取ることができます。
読み取った標準エラー出力を解析することで、エラーの原因を特定し、適切に処理することができます。
また、subprocess.Popen()のcheckパラメータをTrueに設定することで、外部プロセスがエラー終了した場合に例外を発生させることもできます。
実行結果
check=Trueを指定することで、外部プロセスがエラー終了した場合にsubprocess.CalledProcessError例外が発生します。
例外オブジェクトのreturncodeアトリビュートには、外部プロセスのリターンコードが格納されています。
try文とexcept文を使って例外をキャッチすることで、エラー終了した場合の処理を記述することができます。
●標準出力を出力しないことのメリット
ここまで、subprocessモジュールを使って標準出力を出力しない方法を見てきましたが、そもそもなぜ標準出力を出力しないようにするのでしょうか。
実は、標準出力を出力しないようにすることには、いくつかのメリットがあります。
○リソースの節約
まず、リソースの節約が挙げられます。
外部プロセスを実行すると、その標準出力をターミナルに表示するためのリソースが消費されます。
特に、大量の出力が生成される場合は、リソースの消費が無視できなくなります。
例えば、次のようなコードを実行すると、大量の標準出力が生成され、リソースを消費してしまいます。
実行結果
echoコマンドを10000回実行しているので、10000行の標準出力が生成されます。
大量の出力を処理するために、メモリやCPUを消費してしまうのです。
そこで、標準出力を出力しないようにすることで、リソースの消費を抑えることができます。
実行結果
stdout=subprocess.DEVNULLを指定することで、標準出力を/dev/nullにリダイレクトし、出力を抑制しています。
リソースの消費を抑えることができました。
大規模なシステムでは、リソースの消費は大きな問題になります。標準出力を出力しないようにすることで、リソースを節約し、システムのパフォーマンスを向上させることができるのです。
○ログの見通しが良くなる
もう1つのメリットは、ログの見通しが良くなることです。
デバッグやトラブルシューティングの際は、ログを頼りに原因を特定することが多いですよね。
しかし、ログに不要な出力が大量に含まれていると、肝心のログが埋もれてしまい、見つけづらくなってしまいます。
例えば、次のようなコードを実行すると、標準出力とログが混在して出力されてしまいます。
実行結果
lsコマンドの実行結果とログが混在して出力されているため、ログが見づらくなっています。
そこで、標準出力を出力しないようにすることで、ログの見通しを良くすることができます。
実行結果
stdout=subprocess.DEVNULLを指定することで、lsコマンドの実行結果を抑制しています。
ログだけが出力されるようになったので、ログの見通しが良くなりました。
ログの見通しが良くなることで、デバッグやトラブルシューティングの効率が向上します。
問題の原因を素早く特定できるようになるのです。
まとめ
さて、Pythonのsubprocessモジュールを使った外部プロセスの実行における標準出力の制御方法について、一通り見てきましたが、いかがだったでしょうか。
Pythonのsubprocessモジュールを使いこなせるようになれば、システム開発や自動化のシーンでの生産性が飛躍的に向上するはずです。
ぜひ、実務でも活用してみてくださいね。