Pythonのデコレータが面白いのでRubyで実装してみた

・・・けどいまいちだったorz

一応説明すると、デコレータって言うのはPythonにあるアノテーションでメソッドを拡張できる機能・・・らしい。

今回の目標はこれを動かすこと。(@declareArgs(float, float)がデコレータ)
http://www.itmedia.co.jp/enterprise/articles/0501/24/news034_3.html

# http://www.itmedia.co.jp/enterprise/articles/0501/24/news034_3.html
import math

def declareArgs(*argTypes):
 def checkArguments(func):
   assert func.func_code.co_argcount == len(argTypes)
   def wrapper(*args, **kwargs):
   pos = 1
   for (arg, argType) in zip(args, argTypes):
    assert isinstance(arg, argType), \
       "Value %r dose not match %s at %d" % (arg, argType, pos)
    pos += 1
   return func(*args, **kwargs)

  wrapper.func_name = func.func_name
   return wrapper

 return checkArguments

@declareArgs(float, float)
def calcDistance(x, y):
 return math.sqrt(x * x + y * y)

print calcDistance(3.14, 1.592)
print calcDistance(2.0, 4) 

実行結果。

# http://www.itmedia.co.jp/enterprise/articles/0501/24/news034_3.html
$ /opt/Python-2.4/bin/python DecoratorTest.py
3.52052041607
Traceback (most recent call last):
  File "DecoratorTest.py", line 27, in ?
    print calcDistance(2.0, 4)
  File "DecoratorTest.py", line 11, in wrapper
    assert isinstance(arg, argType), \
AssertionError: Value 4 dose not match at 2 

ていうか、なんでITmediaはデコレータの説明なのにわざわざリフレクション使いまくった例にしたんだろう。移植がめんどいよ・・・。

とりあえずデコレータはこんな感じの実装にしてみた。

# decorator.rb
module Kernel
  def decorate(decorator, target)
    decorator = method(decorator) if decorator.is_a? Symbol
    method_name, method_body =
      case target
      when Method
        [target.to_s.sub(/#<Method: (\w+)#(\w+)>/, '\2'), target]
      else
        [target, nil]
      end
    if self.is_a? Class
      method_body ||= instance_method(target).bind(self.new)
      define_method method_name, &decorator.call(method_body)
    else
      method_body ||= method(target)
      self.class.instance_eval{define_method method_name, &decorator.call(method_body)}
    end
  end
end

使ってみたところはこう。

require 'decorator'

def declare_args(*arg_types)
  lambda do |func|
    raise <<-eos.strip unless func.arity == arg_types.size
      The number of arguments, #{func.arity}, should be #{arg_types.size}.
    eos
    lambda do |*args|
      args.size.times do |pos|
        arg, arg_type = args[pos], arg_types[pos]
        raise <<-eos.strip unless arg.instance_of? arg_type
          Value #{arg} does not match #{arg_type} at #{pos}.
        eos
      end
      func.call(*args)
    end
  end
end

def calc_distance(x, y)
  Math.sqrt(x * x + y * y)
end
decorate declare_args(Float, Float), :calc_distance

puts calc_distance(3.14, 1.592)
puts calc_distance(2.0, 4)

実行結果

% ruby dec1.rb
3.52052041607487
dec1.rb:12:in `calc_distance': Value 4 does not match Float at 1. (RuntimeError)
        from dec1.rb:10:in `times'
        from dec1.rb:10:in `calc_distance'
        from dec1.rb:27

んー、まぁ一応動いてるんだけど、アノテートみたいにメソッド定義の前に付けられないのがなぁ・・・。あとデコレータの実装見れば分かるけど、instance_method(target).bind(self.new) ←ここで新しくnewしたオブジェクトをバインドしてるのが最悪。なんとかうまく実行時のインスタンス引っ張って来れないもんか。

まぁ結局、こういうのはActiveSupportのalias_method_chainを使ったほうがいいと思った。