# RubyでPyhonのdecoratorを実装する _published: 2012/07/20_ ![alt](http://b.hatena.ne.jp/entry/image/http://d.hatena.ne.jp/shunsuk/20120720/1342742388) こちらのスライド。RubyでRubyを拡張する。つまりメタプログラミングの話。 - [Extending Ruby with Ruby // Speaker Deck](https://speakerdeck.com/u/michaelfairley/p/extending-ruby-with-ruby) メタプログラミングを使って他の言語の機能を実装しようという内容になってる。 - Python: Function decorators - Scala: Partial application - Haskell: Lazy evaluation 今回は、この中でPythonのdecoratorを実装するというのをピックアップ。 githubにソースコードがあがってて、スライド中では未解決になってた問題がすでに解決されている。 - [michaelfairley/method_decorators](https://github.com/michaelfairley/method_decorators) ここから載せるコードは、githubの方じゃなくてスライド中のコードに一部手を入れたものになってる。ひとつのメソッドに複数のdecoratorを指定できなかったので、そこだけできるようにした。 まずはdecoratorを使う側のコード。 ```ruby class Test extend MethodDecorators +Memoize +Instrument def test sleep 0.5 end end Test.new.test #=> test: 0.500362 ``` メドッソ定義の前に `+decorator名` とすると、しあわせになれる。 `Memoize` はメソッドをメモ化し、 `Instrument` はメソッドの実行時間を出力する。 `Memoize` の方は効果が見えないが、 `Instrument` によって実行時間が出力されている。 これを応用すれば、メソッドの実行ログを出力するdecoratorなど夢が広がる。 その辺は、本家のPythonのdecoratorを調べてみるといいと思う。 次にdecoratorのコード。 ```ruby class Instrument < Decorator def call(orig, *args, &blk) s = Time.now orig.call(*args, &blk).tap { e = Time.now puts "#{orig.name}: #{e - s}" } end end ``` ターゲットなるメソッドが呼ばれる前(厳密に言うと違うけど)に `docorator` の `call` メソッドが呼ばれる。 メソッド本体、引数、ブロックが渡ってくるので、好きなようにゴニョゴニョできる。 上の例でtapを使っているのは、最後にオリジナルのメソッドの戻り値を返すため。 こっちは、メモ化。 ```ruby class Memoize < Decorator def initialize @memo ||= {} end def call(orig, *args, &blk) return @memo[args] if @memo.has_key? args @memo[args] = orig.call(*args, &blk) end end ``` 各 `decorator` は `Decorator` クラスのサブクラスになってる。 `Decorator` クラスの定義はこちら。 ```ruby class Decorator def self.+@ @@decorators ||= [] @@decorators << self.new end def self.decorators @@decorators end def self.clear_decorators @@decorators = nil end end ``` `def .+@` は単項演算子の `+` を定義する。今回はクラスメソッドなので `def self.+@` となる。 `Decorator` のサブクラスのインスタンスを `+` のたびに `Array` に詰めてるだけ。 お待ちかね。`MethodDecorators` のコード。 ```ruby module MethodDecorators def method_added(name) super decorators = Decorator.decorators return unless decorators Decorator.clear_decorators orig_method = instance_method(name) define_method(name) do |*args, &blk| m = orig_method.bind(self) decorators.map {|d| d.call(m, *args, &blk) }.last end end end ``` 解説を入れてみる。 オリジナルのメソッドが呼ばれる時じゃなくて、定義された時の話だということを意識しておくといい。 ```ruby module MethodDecorators # メソッドが追加されるとmethod_addedが呼ばれる def method_added(name) # オーバーライドしてるので、お行儀よくsuperを呼ぶ super # メソッドが追加された時点で+してたdecoratorを取得 decorators = Decorator.decorators # decoratorがなければ終わり return unless decorators # decoratorを削除 Decorator.clear_decorators # オリジナルのメソッドを取得 # 戻り値はレシーバのないUnboundMethodのオブジェクトになる orig_method = instance_method(name) # オリジナルと同じ名前のメソッドを定義 define_method(name) do |*args, &blk| # メソッドのレシーバをself(もともとメソッドが定義されてたクラス)にする m = orig_method.bind(self) # decoratorのcallメソッドを呼んで処理する # 戻り値を書き換えるdecoratorがあった場合、最後のものを優先するようにした # (Pyhonがどうなってるか調べてない) decorators.map {|d| d.call(m, *args, &blk) }.last end end end ``` もういちど、利用側のコードを見てみる。 ```ruby class Test extend MethodDecorators +Memoize +Instrument def test sleep 0.5 end end Test.new.test #=> test: 0.500362 ``` ふつうは module は `import` することが多いんだけど、クラスメソッドを追加したいので `extend` する。 最後に、 `Memoize` と `Instrument` の合わせ技のサンプルを。 やっぱりここはフィボナッチで。 ```ruby class Test extend MethodDecorators def fib(n) if n < 2 n else fibl(n-1) + fib(n-2) end end +Instrument def fib30 fib(30) end end ``` フィボナッチ数列のn番目を求めるメソッド。なにも最適化せずに書いた。 30番目を求める。MacBook Airにこれ以上やらせると、かわいそうだった。 ```ruby puts Test.new.fib30 #=> fib30: 1.052435 #=> 832040 ``` 1秒かかってる。 つぎにMemoizeを追加した場合。 ```ruby class Test extend MethodDecorators + Memoize def fib(n) if n < 2 n else fibl(n-1) + fib(n-2) end end +Instrument def fib30 fib(30) end end ``` 結果は。 ```ruby puts Test.new.fib30 #=> fib30_memo: 0.000656 #=> 832040 ``` 圧倒的。 ネタ元のスライドでは、トランザクションの例が示されてた。 ```ruby class Transactional < Decorator def call(orig, *args, &blk) ActiveRecord::Base.transaction do orig.call(*args, &blk) end end end class Bank extend MethodDecorators +Transactional def send_money(from, to, amount) from.balance -= amount to.balance += amount from.save! to.save! end end ``` Rubyだとこういう書き方しなくてもいろいろ方法はあるんだけど、そのメソッドにどんな側面があるのかというが一目でわかっていんじゃないかと思う。