最近、ある先輩のpythonのコードをレビューさせていただく機会がありまして、 python歴まだ100行くらいの実力でなんとかやってみたのですが、
もしこれがRubyだったらこう書けるのになぁ。。。。 と思うことがすごくあったので、部分的に書き直してみました。
元々のコード
本当にレビューしたものではなく雰囲気似たものを作りました。 このコードの目的はslackから入力を受けつけ、サーバーの情報を取得し、整形して返してslackに表示する、というものです。
server 001 show cpu
と入力がきたら、
%Cpu(s): 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
を返す感じです。
slackとの連携のところは省略して、サーバーの情報を取得し、整形して返す部分を直してみたいと思います。
cli.pyがslackと連携し、ope.pyのメソッドを使う感じです。 ope.pyはサーバーに問いかけ、データを返します。 (※今回はope.pyにテストデータを入れています。)
cli.py
import re import ope from slackbot.bot import respond_to HELP_MSG = 'Available command is [ server server-name show help|cpu|memory|la ]' #slackから以下のような形式のメッセージを受け取ることを前提とする。 #server 001 show cpu @respond_to(u'server.* (.*)', re.I) def status_check_command(message, *args): args = message.lower().split() if len(args) > 3 and args[2] == 'show': info = get_info(args) #message.reply(info) print(info) else: print(HELP_MSG) # ope.pyを使い、データを取得する def get_info(args): if not args[3] in ["cpu", "memory", "la"]: return HELP_MSG table = None if args[3] == 'cpu': cpu_info = ope.get_cpu_info() table = ope.create_cpu_info_table(cpu_info) elif args[3] == 'memory': memory_info = ope.get_memory_info() table = ope.create_memory_info_table(memory_info) elif args[3] == 'la': la_info = ope.get_la_info() table = ope.create_la_info_table(la_info, m=5) if table: return str(table) else: return HELP_MSG
ope.py
# cpu情報の取得 def get_cpu_info(): url = 'cpu/info' ret = get_info(url) return ret def create_cpu_info_table(info): # ... infoを受け取って整形、今回は省略 ... return "cpu: {}".format(info) # memory情報の取得 def get_memory_info(): url = 'memory/info' ret = get_info(url) return ret def create_memory_info_table(info): # ... infoを受け取って整形、今回は省略 ... return "memory: {}".format(info) # load average情報の取得 def get_la_info(): url = 'la/info' ret = get_info(url) return ret def create_la_info_table(info, m=1): # ... infoを受け取って整形、今回は省略 ... # 変化を出すため、load averageは整形部分を少し実装。第二引数を受け取ると、1分の値か5分の値化15分の値かを返してくれます。 if m == 5: m = 3 elif m == 15: m = 4 else: m = None if m: info = "load average: {}".format(info.split()[m]) return "la: {}".format(info) # get_xxx_info系の処理の共通化 def get_info(url): # サーバーにアクセス、データの取得 # 今回はテスト用データ tst = { 'cpu/info': '%Cpu(s): 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st', 'memory/info': 'KiB Mem: 2049988 total, 738980 used, 1311008 free, 71880 buffers', 'la/info': 'load average: 0.00, 0.01, 0.05' } return tst[url]
cpu, memory, laそれぞれのデータを取得用のget_xxx_infoメソッドと、 それぞれのデータの整形用のset_yyy_info_tableメソッドがあります。
Rubyistがみたら興奮するコードですね。
早速Rubyに書き直してみましょう。
sendを使って動的にメソッドを使う
まず、cli.pyのget_infoメソッドを書き換えます。 このメソッドの6行目以降は、どの情報をサーバーから取得するか、ということを目的にしています。 なのでif文で分岐して、目的に合うopeのメソッドを使っています。
これをRubyで書き換えると。。。 cli_ruby.rb
def get_info(args) return "``` #{(if %(cpu, memory, la).include?(args[3]) Ope.send("create_#{args[3]}_info_table", Ope.send("get_#{args[3]}_info"), args[4]) else HELP_MSG end)} ```" end
これだけです。 Rubyのsendを使えば、文字列をそのままメソッド名として使えるので、条件分岐をさせる必要なく、メソッドを使い分けることができます。 入力された文字列がそのまま使えるようにしてあげることで可能になります。
さらにreturnにifを突っ込んであげられるので、わざわざ変数に入れたりすることもなくなりました。
気持ちがいいですね。
20行が5行
になりました。
ゴーストメソッドでメソッドを消す
さて、次はope.pyのget_xxx_info系を直してみたいと思います。
get_xxx_info系のメソッドは、それぞれ違う引数をget_info
に渡しているだけですね。
これらの似たメソッドを全て消してみたい
と思います。
まとめるのではなく、消します。
そして、cli_ruby.rbの方は一切手を加えません。
メソッドを定義せず、メソッドを使う
というわけです。
これを実現させるためには、ゴーストメソッドという手法を使います。
直した後のコードがこちらです。
class Ope class << self def create_cpu_info_table(info) # ... 整形 ... return "cpu #{info}" end def create_memory_info_table(info) # ... 整形 ... return "memory #{info}" end def create_la_info_table(info, *args) # ... 整形 ... # 変化を出すため、load averageは整形部分を少し実装。第二引数を受け取ると、1分の値か5分の値化15分の値かを返してくれます。 m = args[0] unless args.empty? m = m == '1' ? 2 : (m == '5' ? 3 : (m == '15' ? 4 : nil)) return "la load average: #{m ? info.split[m] : info.split(':')[1]}" end def get_info(url) # serverにアクセス、データの取得 tst = { 'cpu/info' => '%Cpu(s): 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st', 'memory/info' => 'KiB Mem: 2049988 total, 738980 used, 1311008 free, 71880 buffers', 'la/info' => 'load average: 0.00, 0.01, 0.05' } return tst[url] end def method_missing(method_id, *args, &block) if /get_(.*)_info/ =~ method_id.to_s get_info("#{$1}/info") else super end end end end
一番下に定義してあるmethod_missing
に注目です。
このメソッドは、定義されていないメソッドが呼び出されたら最終的に行き着くメソッドです。
本来はいつもみているNoMethodError
を吐き出しているのですが、オーバーライドしてあげることで、その動作を書き換えられます。
ここでは、get_xxx_info系のメソッドが呼びされたら、get_infoにメソッドの名前から抽出したパスを渡しています。直す前のget_xxx_info系と同じ動きですね。
これでget_xxx_info系のメソッドを全て消す
ことができました。
create_yyy_info_table系もやっても良かったのですが、このメソッドはそれぞれ整形のルールがかなり違うため、やめておきます。
こういう判断も大切です。
ゴーストメソッドとは関係がないですが、create_la_info_table
の3行目の三項演算子をひたすら重ねるのも気に入ってます。
pythonは三項演算子がないので、どうしても行が多くなりますね。
ope.pyは、rubyで書き直すと、50行から38行になりました。 メソッドを定義してないのにメソッドが使えるのは、やはりかっこいいですね。
まとめ
結果として、 cli.pyのget_infoメソッドはRubyに直すと75%カット ope.pyのget_xxx_infoメソッドは24%カットできました。
かなりスリムになりました。 同じことをするコードをここまでスリムにできるのはかなりお得になった気がします。
やっぱRubyだなぁ〜と思う、そんな僕でした。