やっはろー。
###要約
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
なんかで拾ってあげれば例外処理が書けます。
本記事ではディスってますがめっちゃ便利なモジュールですので初めて知られた方は是非使ってみて下さい。
処理も普通に使う分にはそこまで重くないです。(企画崩壊)
#subprocess
でcat
は愚行だった
前述のように、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を監視するスクリプトを書きました。
#最後に
思っていたより明確な差が出て驚きました。
もっともっと勉強して「適材適所」を意識したプログラムを書けるようになりたいです。