LoginSignup
6
3

More than 3 years have passed since last update.

やはりpythonのsubprocessでcatするのはまちがっている。

Last updated at Posted at 2020-08-15

やっはろー。

要約

pythonでsubprocess.run('cat path', shell=True)なんて書いている人は、僕と一緒に悔い改めて明日からopen()を使いましょう。

subprocessとは

お詳しい方は読み飛ばしてください。
subprocessとはLinuxのOSコマンドをpython上から実行するモジュールです。
Python上でOSコマンドを実行するにはos関数やsystem関数などもありますが現在はsubprocessが推奨されているそうです。

import subprocess

list = subprocess.run('ls') 
#hoge.py  hoge.sh  hoge.wav
print(list)
#CompletedProcess(args='ls', returncode=0)

#引数にコマンドを渡してあげると実行結果(ステータス)が返ってきます

list = subprocess.run('ls', encoding='utf-8', stdout=subprocess.PIPE).stdout
print(list)
#hoge.py
#hoge.sh
#hoge.wav

#encodingを指定することでutf-8などでエンコードできます
#stdout属性を指定することで標準出力に出力を渡せます

list = subprocess.check_output('ls -a', shell=True, encoding='utf-8')
print(list)
#.hoge
#hoge.py
#hoge.sh
#hoge.wav

#shell=Trueを指定することで空白を含められますが、コマンドインジェクションなどの危険があります
#subprocess.check_output(...) == subprocess.run(..., check=True, stdout=PIPE).stdout

こんな感じに色々とOSコマンドを実行できますが、毎回サブプロセスを作成するので処理が遅いです。
イメージとしては、親が本を読むためにわざわざ子供を産んで子供に音読させてるような感じです。
自分で読めば解決するのに、そりゃ重いのも納得ですね。

ちなみにstdout=subprocess.PIPEのオプションは音読してね!って意味です。
親は子供の読んでいる本の内容を(第六感で思念を読めない限り)知ることができませんが、音読をしてもらえば知ることができますね。
subprocess.check_output(...)subprocess.run(..., check=True, stdout=PIPE).stdoutと等価です。
check=True属性はコマンドが非ゼロ(エラー)終了した際にCalledProcessError例外が送出されるのでexceptionなんかで拾ってあげれば例外処理が書けます。

本記事ではディスってますがめっちゃ便利なモジュールですので初めて知られた方は是非使ってみて下さい。
処理も普通に使う分にはそこまで重くないです。(企画崩壊)

subprocesscatは愚行だった

前述のように、subprocess.run('cat /hogehoge.txt', shell=True)とすればhogehoge.txtのテキストコンテンツが取得できますが、テキストを引っ張ってくるためだけにサブプロセスを立ち上げるので処理が遅くなってしまいます。
Pythonでファイルを開くにはopen()という組み込み関数が使えますのでそちらを使いましょう。
超々初歩的なことですが自分は知りませんでした、簡単な使い方は以下の通り。

with open('/hogehoge.txt') as f:
    hoge = f.read()
print(hoge)
#/hogehoge.txtのテキストが表示されます

open()で開いたものは必ずclose()で閉じなくてはいけないので、withを用いて閉じ忘れをなくしています。
open()を閉じた後、ファイルは参照できなくなりますが代入した変数は参照できます。
詳しくは公式ドキュメントをご覧ください。

open()はどれくらい早いのか

以下のようなスクリプトを書いて検証してみました。

#!/usr/bin/python3.7

import subprocess
import time

#open()
start = time.time() #開始時間取得
for i in range(1000): #1000回繰り返し実行
    with open('/sys/class/thermal/thermal_zone0/temp') as f: #ファイルを開く
        test = f.read() #ファイルのテキストを代入
elapsed_time = time.time() - start #終了時刻を取得して開始時刻との差分を求める
print("Open: {}s".format(elapsed_time)) #経過時間を書き出し

#subprocess
start = time.time()
for i in range(1000):
    test = subprocess.run('cat /sys/class/thermal/thermal_zone0/temp', shell=True, stdout=subprocess.PIPE).stdout
elapsed_time = time.time() - start
print("Subprocess: {}s".format(elapsed_time))

#subprocessでvcgencmd(CPU温度に関して参考記録)
start = time.time()
for i in range(1000):
    test = subprocess.run('vcgencmd measure_temp', shell=True, stdout=subprocess.PIPE).stdout
elapsed_time = time.time() - start
print("Vcgencmd: {}s".format(elapsed_time))

実行結果は以下の通りです。

Open: 0.08225083351135254s
Subprocess: 5.188024044036865s
Vcgencmd: 6.032892465591431s

なんということでしょう!
あんなに遅かったsubprocessが50倍以上速くなりました。

恐らく経過時間の内訳は大方待機時間だと思いますのでCPU負荷に関しては単純には言えないのですが、別窓から監視していた感じ軽いのは確かです。
数回実行するとばらつきはありますが、ここまで違ってくるとopen()を使おうという気になります。

ちなみに、subprocess.runでは親プロセスの処理を子プロセスが終了するまで停止させますが、subprocess.Popenを用いることで子プロセスをバックグラウンドに回して並列処理が行えます。
subprocess.Popenを用いても上記検証では2sほどかかっていたのでopen()が速いのは間違いないのですが、状況によっては差が縮まることもあります。
いずれにせよ今回の目的はテキストファイルの取得ですから並列は避けたいですが…。

本検証はPythonでラズパイのCPU温度を測定するという記事で扱ったスクリプトの改良をきっかけとしているので参考にvcgencmdでも確認してみました。
vcgencmdの出力は温度の値が可読性が高くなるように整形されているので、その分遅くなっていると考えられます。

【追記】
本記事の内容をもとにPythonでCPUを監視するスクリプトを書きました。

最後に

思っていたより明確な差が出て驚きました。
もっともっと勉強して「適材適所」を意識したプログラムを書けるようになりたいです。

6
3
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
6
3