# RubyでPyhonのdecoratorを実装する
_published: 2012/07/20_ 
こちらのスライド。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だとこういう書き方しなくてもいろいろ方法はあるんだけど、そのメソッドにどんな側面があるのかというが一目でわかっていんじゃないかと思う。