読み込み中...

Pythonにおけるsubprocessの標準出力を出力しないベストプラクティス6選

subprocessの徹底解説画像 Python
この記事は約19分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

Pythonでシステム開発や自動化スクリプトを作成している方、subprocessモジュールを使って外部プロセスを実行している方は多いのではないでしょうか。

subprocessモジュールは非常に便利で強力なツールですが、標準出力の扱いに悩んだことはありませんか?

私も以前、subprocessを使ったスクリプトを作成していた際、標準出力が大量に出力されてログが見づらくなったり、リソースを無駄に消費したりしてしまった経験があります。

そこで今回は、Pythonのsubprocessモジュールを使用する際に、標準出力を出力しないためのベストプラクティスを6つ紹介したいと思います。

コードサンプルを交えながら、初心者の方にもわかりやすく解説していきますので、ぜひ最後までお付き合いください。

○subprocessの標準出力とは

subprocessモジュールを使って外部プロセスを実行すると、そのプロセスが標準出力に出力した内容がPythonのプログラムにも表示されます。

これが「標準出力」です。

例えば、次のようなコードを実行すると、lsコマンドの実行結果が標準出力に表示されます。

import subprocess

subprocess.run(["ls", "-l"])

実行結果

total 24
-rw-r--r-- 1 user group 1024 Apr 1 12:34 file1.txt
-rw-r--r-- 1 user group 2048 Apr 1 12:35 file2.txt
-rw-r--r-- 1 user group 4096 Apr 1 12:36 file3.txt

このように、subprocessを使って外部プロセスを実行すると、その標準出力がPythonのプログラムにも表示されてしまうのです。

○なぜ標準出力を制御する必要があるのか

では、なぜ標準出力を制御する必要があるのでしょうか。

主に次の2つの理由が挙げられます。

  1. 不要な出力によるログの混乱を避けるため
  2. リソースを効率的に使用し、パフォーマンスを最適化するため

1つ目の理由は、先ほどの例のように、外部プロセスの標準出力がそのままPythonのプログラムに表示されてしまうと、本来出力したいログが埋もれてしまい、ログが見づらくなってしまうことです。

特に、大量の標準出力が生成される場合は深刻です。

2つ目の理由は、標準出力を出力するためにはリソースを消費するため、パフォーマンスに影響を与えてしまうことです。

特に、頻繁に外部プロセスを実行するようなプログラムでは、標準出力の制御は重要になってきます。

●subprocess.run()で標準出力を出力しない方法

それでは、subprocess.run()を使って外部プロセスを実行する際に、標準出力を出力しない方法を見ていきましょう。

subprocess.run()は、Python 3.5以降で導入された比較的新しい関数で、外部プロセスの実行を簡単に行うことができます。

従来のsubprocess.call()やsubprocess.check_call()の代わりに使用することが推奨されています。

subprocess.run()を使って標準出力を出力しないようにするには、次の2つの方法があります。

  1. stdout=subprocess.DEVNULLを指定する
  2. capture_output=Trueを指定する

それぞれ見ていきましょう。

○サンプルコード1:stdout=subprocess.DEVNULL

1つ目の方法は、subprocess.run()の引数にstdout=subprocess.DEVNULLを指定する方法です。

subprocess.DEVNULLは、Unixシステムにおける/dev/nullと同様の機能を持つPythonの特殊ファイルです。

/dev/nullは、「ビットバケット」とも呼ばれ、書き込まれたデータを全て破棄します。

つまり、標準出力をsubprocess.DEVNULLにリダイレクトすることで、出力を抑制することができるのです。

ここでは、lsコマンドを実行し、その標準出力を抑制する例を紹介します。

import subprocess

subprocess.run(["ls", "-l"], stdout=subprocess.DEVNULL)

実行結果

(何も出力されない)

stdout=subprocess.DEVNULLを指定したことで、lsコマンドの実行結果が標準出力に出力されないことがわかります。

ちなみに、stderr=subprocess.DEVNULLを指定すれば、標準エラー出力も同様に抑制することができます。

import subprocess

subprocess.run(["ls", "-l", "nonexistent_file"], stdout=subprocess.DEVNULL, 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コマンドを実行し、その標準出力をキャプチャする例を紹介します。

import subprocess

result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
print(result.stdout)

実行結果

total 24
-rw-r--r-- 1 user group 1024 Apr 1 12:34 file1.txt
-rw-r--r-- 1 user group 2048 Apr 1 12:35 file2.txt
-rw-r--r-- 1 user group 4096 Apr 1 12:36 file3.txt

capture_output=Trueを指定したことで、lsコマンドの実行結果がresult.stdoutにキャプチャされ、print()で出力することができました。

ここで、text=Trueを指定していることに注目してください。

text=Trueを指定すると、stdoutとstderrがバイト列ではなく文字列として返されます。

text=Trueを指定しない場合は、stdoutとstderrをデコードする必要があります。

import subprocess

result = subprocess.run(["ls", "-l"], capture_output=True)
print(result.stdout.decode())

実行結果

total 24
-rw-r--r-- 1 user group 1024 Apr 1 12:34 file1.txt
-rw-r--r-- 1 user group 2048 Apr 1 12:35 file2.txt
-rw-r--r-- 1 user group 4096 Apr 1 12:36 file3.txt

result.stdout.decode()とすることで、バイト列を文字列にデコードしています。

capture_output=Trueを指定すれば、標準出力と標準エラー出力をプログラム内で利用することができます。

これは、外部プロセスの実行結果を解析したい場合などに便利です。

ただし、capture_output=Trueを指定しても、標準出力と標準エラー出力がターミナルに表示されてしまうことがあります。

その場合は、stdout=subprocess.PIPEとstderr=subprocess.PIPEを併用しましょう。

import subprocess

result = subprocess.run(["ls", "-l"], capture_output=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print(result.stdout)

実行結果

total 24
-rw-r--r-- 1 user group 1024 Apr 1 12:34 file1.txt
-rw-r--r-- 1 user group 2048 Apr 1 12:35 file2.txt
-rw-r--r-- 1 user group 4096 Apr 1 12:36 file3.txt

stdout=subprocess.PIPEとstderr=subprocess.PIPEを指定することで、標準出力と標準エラー出力がパイプを通じてプログラムにリダイレクトされ、ターミナルには表示されなくなります。

●subprocess.Popen()で標準出力を出力しない方法

次は、subprocess.Popen()を使って外部プロセスを実行する際に、標準出力を出力しない方法を見ていきましょう。

subprocess.Popen()は、subprocess.run()と比べてより低レベルなインターフェースを提供しており、外部プロセスの実行をより細かく制御することができます。

subprocess.Popen()を使って標準出力を出力しないようにするには、次の2つの方法があります。

  1. stdout=subprocess.PIPEを指定する
  2. stdout=subprocess.DEVNULLを指定する

それぞれ見ていきましょう。

○サンプルコード3:stdout=subprocess.PIPE

1つ目の方法は、subprocess.Popen()のstdout引数にsubprocess.PIPEを指定する方法です。

subprocess.PIPEは、外部プロセスの標準出力をパイプを通じてプログラムに渡すための特殊値です。

stdout=subprocess.PIPEを指定することで、外部プロセスの標準出力をプログラム内で読み取ることができます。

ここでは、lsコマンドを実行し、その標準出力をプログラム内で読み取る例を紹介します。

import subprocess

proc = subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE, text=True)
output = proc.communicate()[0]
print(output)

実行結果

total 24
-rw-r--r-- 1 user group 1024 Apr 1 12:34 file1.txt
-rw-r--r-- 1 user group 2048 Apr 1 12:35 file2.txt
-rw-r--r-- 1 user group 4096 Apr 1 12:36 file3.txt

まず、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コマンドを実行し、その標準出力を抑制する例を見ていきましょう。

import subprocess

proc = subprocess.Popen(["ls", "-l"], stdout=subprocess.DEVNULL)
proc.wait()

実行結果

(何も出力されない)

subprocess.Popen()でlsコマンドを実行し、stdout=subprocess.DEVNULLを指定することで、標準出力を/dev/nullにリダイレクトしています。

proc.wait()を呼び出すことで、外部プロセスの実行が終了するまで待ちます。

標準出力が/dev/nullにリダイレクトされているため、何も出力されません。

●標準出力のリダイレクト

ここまで、subprocess.run()とsubprocess.Popen()を使って標準出力を出力しない方法を見てきましたが、標準出力を別のファイルにリダイレクトすることもできます。

標準出力をファイルにリダイレクトすることで、外部プロセスの実行結果を後から確認したり、ログとして保存したりすることができます。

○サンプルコード5:stdoutをファイルにリダイレクト

ここでは、lsコマンドを実行し、その標準出力をファイルにリダイレクトする例を紹介します。

import subprocess

with open("output.txt", "w") as f:
    proc = subprocess.Popen(["ls", "-l"], stdout=f, text=True)
    proc.wait()

実行結果

(何も出力されない)

ファイルの内容(output.txt)

total 24
-rw-r--r-- 1 user group 1024 Apr 1 12:34 file1.txt
-rw-r--r-- 1 user group 2048 Apr 1 12:35 file2.txt
-rw-r--r-- 1 user group 4096 Apr 1 12:36 file3.txt

まず、open()関数を使って、出力先のファイル(output.txt)をwモード(書き込みモード)で開きます。

with文を使っているので、ファイルを閉じる処理は自動的に行われます。

次に、subprocess.Popen()でlsコマンドを実行しますが、ここでstdout引数にファイルオブジェクト(f)を指定しています。

text=Trueを指定しているので、ファイルには文字列として書き込まれます。

proc.wait()を呼び出して、外部プロセスの実行が終了するまで待ちます。

実行結果を見ると、標準出力は何も出力されていません。

代わりに、output.txtファイルにlsコマンドの実行結果が書き込まれています。

このように、標準出力をファイルにリダイレクトすることで、外部プロセスの実行結果を保存することができます。

ただし、ファイルにリダイレクトする場合は、ファイルのパーミッションに注意が必要です。

書き込み権限がない場合はエラーになります。

また、ファイルがすでに存在する場合は、上書きされてしまうので注意しましょう。

必要に応じて、ファイルをaモード(追記モード)で開くことで、ファイルの末尾に追記することもできます。

import subprocess

with open("output.txt", "a") as f:
    proc = subprocess.Popen(["ls", "-l"], stdout=f, text=True)
    proc.wait()

以上のように、標準出力をファイルにリダイレクトすることで、外部プロセスの実行結果を保存することができます。

●エラー処理

外部プロセスを実行する際は、エラーが発生する可能性があります。

エラーが発生した場合は、適切に処理する必要があります。

○サンプルコード6:stderr=subprocess.PIPE

ここでは、存在しないコマンドを実行し、標準エラー出力をプログラム内で読み取る例を紹介します。

import subprocess

proc = subprocess.Popen(["nonexistent_command"], stderr=subprocess.PIPE, text=True)
_, stderr = proc.communicate()
print(f"標準エラー出力:{stderr}")

実行結果

標準エラー出力:/bin/sh: nonexistent_command: command not found

ここでは、subprocess.Popen()で存在しないコマンド(nonexistent_command)を実行しています。

stderr引数にsubprocess.PIPEを指定することで、標準エラー出力をパイプに接続しています。

proc.communicate()を呼び出すことで、外部プロセスの実行が終了するまで待ち、パイプから標準出力と標準エラー出力を読み取ります。

ここでは、標準エラー出力だけを読み取っているので、proc.communicate()[1]で標準エラー出力を取得しています。

最後に、取得した標準エラー出力をprint()で出力しています。

存在しないコマンドを実行したので、標準エラー出力にエラーメッセージが表示されています。

このように、stderr=subprocess.PIPEを指定することで、標準エラー出力をプログラム内で読み取ることができます。

読み取った標準エラー出力を解析することで、エラーの原因を特定し、適切に処理することができます。

また、subprocess.Popen()のcheckパラメータをTrueに設定することで、外部プロセスがエラー終了した場合に例外を発生させることもできます。

import subprocess

try:
    proc = subprocess.Popen(["nonexistent_command"], check=True)
except subprocess.CalledProcessError as e:
    print(f"外部プロセスがエラー終了しました。リターンコード:{e.returncode}")

実行結果

外部プロセスがエラー終了しました。リターンコード:127

check=Trueを指定することで、外部プロセスがエラー終了した場合にsubprocess.CalledProcessError例外が発生します。

例外オブジェクトのreturncodeアトリビュートには、外部プロセスのリターンコードが格納されています。

try文とexcept文を使って例外をキャッチすることで、エラー終了した場合の処理を記述することができます。

●標準出力を出力しないことのメリット

ここまで、subprocessモジュールを使って標準出力を出力しない方法を見てきましたが、そもそもなぜ標準出力を出力しないようにするのでしょうか。

実は、標準出力を出力しないようにすることには、いくつかのメリットがあります。

○リソースの節約

まず、リソースの節約が挙げられます。

外部プロセスを実行すると、その標準出力をターミナルに表示するためのリソースが消費されます。

特に、大量の出力が生成される場合は、リソースの消費が無視できなくなります。

例えば、次のようなコードを実行すると、大量の標準出力が生成され、リソースを消費してしまいます。

import subprocess

for i in range(10000):
    subprocess.run(["echo", f"Output {i}"])

実行結果

Output 0
Output 1
Output 2
...
Output 9997
Output 9998
Output 9999

echoコマンドを10000回実行しているので、10000行の標準出力が生成されます。

大量の出力を処理するために、メモリやCPUを消費してしまうのです。

そこで、標準出力を出力しないようにすることで、リソースの消費を抑えることができます。

import subprocess

for i in range(10000):
    subprocess.run(["echo", f"Output {i}"], stdout=subprocess.DEVNULL)

実行結果

(何も出力されない)

stdout=subprocess.DEVNULLを指定することで、標準出力を/dev/nullにリダイレクトし、出力を抑制しています。

リソースの消費を抑えることができました。

大規模なシステムでは、リソースの消費は大きな問題になります。標準出力を出力しないようにすることで、リソースを節約し、システムのパフォーマンスを向上させることができるのです。

○ログの見通しが良くなる

もう1つのメリットは、ログの見通しが良くなることです。

デバッグやトラブルシューティングの際は、ログを頼りに原因を特定することが多いですよね。

しかし、ログに不要な出力が大量に含まれていると、肝心のログが埋もれてしまい、見つけづらくなってしまいます。

例えば、次のようなコードを実行すると、標準出力とログが混在して出力されてしまいます。

import subprocess
import logging

logging.basicConfig(level=logging.INFO)

# 外部プロセスを実行
subprocess.run(["ls", "-l"])

# ログを出力
logging.info("処理を完了しました")

実行結果

total 24
-rw-r--r-- 1 user group 1024 Apr 1 12:34 file1.txt
-rw-r--r-- 1 user group 2048 Apr 1 12:35 file2.txt
-rw-r--r-- 1 user group 4096 Apr 1 12:36 file3.txt
INFO:root:処理を完了しました

lsコマンドの実行結果とログが混在して出力されているため、ログが見づらくなっています。

そこで、標準出力を出力しないようにすることで、ログの見通しを良くすることができます。

import subprocess
import logging

logging.basicConfig(level=logging.INFO)

# 外部プロセスを実行(標準出力を抑制)
subprocess.run(["ls", "-l"], stdout=subprocess.DEVNULL)

# ログを出力
logging.info("処理を完了しました")

実行結果

INFO:root:処理を完了しました

stdout=subprocess.DEVNULLを指定することで、lsコマンドの実行結果を抑制しています。

ログだけが出力されるようになったので、ログの見通しが良くなりました。

ログの見通しが良くなることで、デバッグやトラブルシューティングの効率が向上します。

問題の原因を素早く特定できるようになるのです。

まとめ

さて、Pythonのsubprocessモジュールを使った外部プロセスの実行における標準出力の制御方法について、一通り見てきましたが、いかがだったでしょうか。

Pythonのsubprocessモジュールを使いこなせるようになれば、システム開発や自動化のシーンでの生産性が飛躍的に向上するはずです。

ぜひ、実務でも活用してみてくださいね。