Rubyだったら。。。。。。

最近、ある先輩の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だなぁ〜と思う、そんな僕でした。