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を使ったほうがいいと思った。