RubyをRubyで拡張する:Pythonから関数デコレーターを借りる

翻訳者から: Michael Fairley-RubyでRubyを拡張するのプレゼンテーションの冒頭を翻訳することをお勧めします。 私の意見では、実用的な価値と利益が最大であるため、3つの最初の部分のみを翻訳しました。 それでも、私は、Pythonに加えてHaskellとScalaからチップを借りる例を提供する完全なプレゼンテーションに精通することを強くお勧めします。



関数デコレータ



Pythonには、デコレータがあります。デコレータは、頻繁に使用される機能の一部をメソッドや関数に追加するための構文糖衣です。 ここで、デコレータとは何か、Rubyでデコレータが役立つ理由の例をいくつか示します。



私は以前Pythonで多くの仕事をしていましたが、関数デコレーターはそれ以来間違いなく欠落しているものであり、それだけでなく、ほとんどすべての人がRubyコードをきれいにするのに役立ちます。



Rubyを利用して、ある銀行口座から別の銀行口座に送金する必要があるふりをします。 すべてがシンプルに見えますよね?



def send_money(from, to, amount) from.balance -= amount to.balance += amount from.save! to.save! end
      
      





from 」アカウントの残高から金額を差し引きます...



 from.balance -= amount
      
      





そして、この金額を「 to 」アカウントの残高に追加します...



 to.balance += amount
      
      





そして、両方のアカウントを保存します。



 from.save! to.save!
      
      





しかし、いくつかの欠点があります。最も明白なのはトランザクションの欠如です(「 from.save! 」が成功した場合、「 to.save! 」がそうでない場合、お金は空中に消えます )。



幸いなことに、ActiveRecordはこの問題の解決を非常に簡単にします。 トランザクションメソッドのブロックにコードをラップするだけで、ブロック内ですべてが正常に終了するかどうかが保証されます。



 def send_money(from, to, amount) ActiveRecord::Base.transaction do from.balance -= amount to.balance += amount from.save! to.save! end end
      
      







Pythonで同じ例を見てみましょう。 トランザクションのないバージョンは、Rubyとほぼ同じように見えます。



 def send_money(from, to, amount): from.balance -= amount to.balance += amount from.save() to.save()
      
      





しかし、トランザクションを追加する価値があり、コードはもはやエレガントではないように見え始めます。



 def send_money(from, to, amount): try: db.start_transaction() from.balance -= amount to.balance += amount from.save() to.save() db.commit_transaction() except: db.rollback_transaction() raise
      
      





このメソッドには10行のコードがありますが、ビジネスロジックを実装するのはそのうちの4行のみです。



 from.balance -= amount to.balance += amount from.save() to.save()
      
      





他の6行は、トランザクション内でロジックを起動するためのテンプレートです。 これはくて冗長すぎますが、さらに悪いことに、正しいエラー処理やロールバックのセマンティクスなど、これらすべての行を覚えておく必要があります。



 def send_money(from, to, amount): try: db.start_transaction() ... db.commit_transaction() except: db.rollback_transaction() raise
      
      







では、どうすればそれをより美しくし、自分自身の繰り返しを少なくするのでしょうか? Pythonにはブロックがないため、Rubyのようなフォーカスはここでは機能しません。 ただし、Pythonにはメソッドを簡単に渡したり再割り当てしたりする機能があります。 したがって、別の関数を引数として取り、同じ関数を返しますが、既にテンプレートトランザクションコードでラップされている「 トランザクション 」関数を作成できます。



 def send_money(from, to, amount): from.balance -= amount to.balance += amount from.save() to.save() send_money = transactional(send_money)
      
      





そして、ここにトランザクション関数がどのように見えるかがあります...



 def transactional(fn): def transactional_fn(*args): try: db.start_transaction() fn(*args) db.commit_transaction() except: db.rollback_transaction() raise return transactional_fn
      
      





関数(この例では " send_money ")を唯一の引数として受け取ります。



 def transactional(fn):
      
      





新しい関数を定義します。



 def transactional_fn(*args):
      
      





新しい関数には、トランザクションでビジネスロジックをラップするためのテンプレートが含まれています。



 try: db.start_transaction() ... db.commit_transaction() except: db.rollback_transaction() raise
      
      





テンプレート内で、元の関数が呼び出され、新しい関数に渡された引数が渡されます。



 fn(*args)
      
      





最後に、新しい機能が復活しました。



 return transactional_fn
      
      





したがって、 send_money関数を先ほど定義したトランザクション関数に渡します。 この関数は、 send_money関数と同じことを実行する新しい関数を返しますが、すべてトランザクション内で実行します。 そして、この新しい関数をsend_money関数に割り当て、元のコンテンツをオーバーライドします。 これで、send_money関数を呼び出すたびに、トランザクションバージョンが呼び出されます。



 send_money = transactional(send_money)
      
      





そして、これは私がこれまでずっとリードしてきたことです。 このイディオムはPythonで頻繁に使用されるため、特別な構文である関数デコレータが追加されてサポートされています。 そして、それがDjango ORMでトランザクションを行う方法です。



 @transactional def send_money(from, to, amount): from.balance -= amount to.balance += amount from.save() to.save()
      
      







それで何?



今、あなたは考えています。 この装飾mumba-yumbaが、ブロックが解決するのと同じ問題をどのように解決するかを示しました。 Rubyでこの帽子が必要なのはなぜですか?」では、ブロックがそれほどエレガントに見えなくなった場合を見てみましょう。



フィボナッチ数列のn番目の要素の値を計算する方法を考えましょう。



 def fib(n) if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
      
      





彼は遅いので、私たちは彼を覚えておきたい。 これに対して一般的に受け入れられているアプローチは、トランザクションで最初の例と同じ病気に悩まされる「 || = 」をどこにでも押し出すことです。



 def fib(n) @fib ||= {} @fib[n] ||= if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
      
      





さらに、「nil」と「false」をこのように記憶できないという事実など、ここでいくつかのことを忘れました:常に覚えておく必要がある別のポイント。



 def fib(n) @fib ||= {} return @fib[n] if @fib.has_key?(n) @fib[n] = if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
      
      





まあ、これはブロックで解決できますが、ブロックはそれらを呼び出す関数の名前や引数にアクセスできないため、この情報を明示的に渡す必要があります。



 def fib(n) memoize(:fib, n) do if n <= 1 1 else fib(n - 1) + fib(n - 2) end end end
      
      





そして今、コア機能の周りにさらにブロックを追加し始めると...



 def fib(n) memoize(:fib, n) do time(:fib, n) do if n <= 1 1 else fib(n - 1) + fib(n - 2) end end end end
      
      





...メソッド名とその引数を何度も再入力する必要があります。



 def fib(n) memoize(:fib, n) do time(:fib, n) do synchronize(:fib) do if n <= 1 1 else fib(n - 1) + fib(n - 2) end end end end end
      
      





これはかなり壊れやすい構造であり、メソッドのシグネチャを何らかの方法で変更することにした瞬間を壊します。



それでも、これはメソッドの定義の直後にそのようなものを追加することで解決できます。



 def fib(n) if n <= 1 1 else fib(n - 1) + fib(n - 2) end end ActiveSupport::Memoizable.memoize :fib
      
      





そして、これはPythonで見たものを思い出させるはずです-メソッドの変更がメソッド自体の直後に行われたとき。



 # Ruby def fib(n) ... end ActiveSupport::Memoizable.memoize :fib # Python def fib(n): ... fib = memoize(fib)
      
      





Pythonコミュニティがこのソリューションを好まなかったのはなぜですか? 2つの理由:



Pythonでのフィボナッチの例を見てみましょう。



 def fib(n): if n <= 1: return 1 else return fib(n - 1) + fib(n - 2)
      
      





私たちはそれをメモしたいので、 メモ機能でそれを飾ります。



 @memoize def fib(n): if n <= 1: return 1 else return fib(n - 1) + fib(n - 2)
      
      





そして、メソッドの実行時間を測定したり、呼び出しを同期したい場合は、別のデコレーターを追加します。 以上です。



 @synchronize @time @memoize def fib(n): if n <= 1: return 1 else return fib(n - 1) + fib(n - 2)
      
      





そして、Rubyでこれを実現する方法を説明します(「@」の代わりに「+」と大文字の最初の文字を使用)。 最も面白いのは、このデコレータ構文をRubyに追加できることです。Rubyは、わずか15行のコードでPythonの構文に非常に近いものです。



 +Synchronized +Timed +Memoized def fib(n) if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
      
      







ダイビング



send_moneyの例に戻りましょう。 Transactionalデコレータを追加します。



 +Transactional def send_money(from, to, amount) from.balance -= amount to.balance += amount from.save! to.save! end
      
      





Transactionalは、以下で説明するDecoratorのサブクラスです。



 class Transactional < Decorator def call(orig, *args, &blk) ActiveRecord::Base.transaction do orig.call(*args, &blk) end end end
      
      





彼には呼び出しメソッドが1つしかなく、元のメソッドの代わりに呼び出されます。 引数として、彼は「ラップ」するメソッド、引数、ブロックを受け取ります。これらは呼び出されたときに渡されます。



 def call(orig, *args, &blk)
      
      





トランザクションを開きます。



 ActiveRecord::Base.transaction do
      
      





そして、トランザクションブロック内で元のメソッドを呼び出します。



 orig.call(*args, &blk)
      
      





デコレータの構造は、Pythonでのデコレータの動作とは異なることに注意してください。 引数を受け取る新しい関数を定義する代わりに、Rubyデコレータは呼び出しごとにメソッド自体とその引数を受け取ります。 Rubyでオブジェクトにメソッドをバインドするセマンティクスのため、これを行うことを強制されます。これについては、以下で説明します。



クラス「 デコレータ 」の中には何がありますか?



 class Decorator def self.+@ @@decorator = self.new end def self.decorator @@decorator end def self.clear_decorator @@decorator = nil end end
      
      





この「 + @ 」は「単項プラス」演算子であるため、 + Transactionalで行ったように、このメソッドは+ DecoratorNameを呼び出すときに呼び出されます。



 def self.+@
      
      





また、現在のデコレータを取得する方法も必要です。



 def self.decorator @@decorator end
      
      





そして、現在のデコレータをリセットする方法。



 def self.clear_decorator @@decorator = nil end
      
      





装飾されたメソッドを必要とするクラスは、 MethodDecoratorsモジュールによって拡張する必要があります。



 class Bank extend MethodDecorators +Transactional def send_money(from, to, amount) from.balance -= amount to.balance += amount from.save! to.save! end end
      
      





Classクラスをすぐに拡張することは可能ですが、この場合のベストプラクティスは、そのような決定をエンドユーザーの裁量に任せることだと思います。



 module MethodDecorators def method_added(name) super decorator = Decorator.decorator return unless decorator Decorator.clear_decorator orig_method = instance_method(name) define_method(name) do |*args, &blk| m = orig_method.bind(self) decorator.call(m, *args, &blk) end end end
      
      





Method_added ”は、新しいメソッドがクラスで定義されるたびに呼び出されるクラスのプライベートメソッドであり、メソッドの作成時にキャッチする便利な方法を提供します。



 def method_added(name)
      
      





method_addedを呼び出します。 「 method_added 」、「 method_missing 」、「 respond_to? 」などのメソッドをオーバーライドすることで、このことを簡単に忘れることができます 「しかし、そうしないと、他のライブラリを簡単に破ることができます。



 super
      
      





現在のデコレータを取得し、デコレータがない場合は関数を終了し、そうでない場合は現在のデコレータをゼロにします。 デコレータをリセットすることが重要です。それは、メソッドを再定義し、再び「 method_added 」を呼び出すためです。



 decorator = Decorator.decorator return unless decorator Decorator.clear_decorator
      
      





メソッドの元のバージョンを抽出します。



 orig_method = instance_method(name)
      
      





そして、それを再定義します。



 define_method(name) do |*args, &blk|
      
      





Instance_method 」は、実際には「 UnboundMethod 」クラスのオブジェクトを返します。これは、どのオブジェクトに属しているかを知らないメソッドであるため、現在のオブジェクトにバインドする必要があります。



 m = orig_method.bind(self)
      
      





そして、デコレータを呼び出し、元のメソッドと引数を渡します。



 decorator.call(m, *args, &blk)
      
      







他に何?



もちろん、このコードが本番稼働の準備ができていると見なされる前に解決しなければならない非常に重要なポイントがいくつかあります。



複数のデコレータ


私が引用した実装では、各メソッドに対して1つのデコレータのみを使用できますが、複数のデコレータを使用できるようにしたいと考えています。



 +Timed +Memoized def fib(n) ... end
      
      







範囲


Define_methodはパブリックメソッドを定義しますが、スコープに関して装飾できるプライベートメソッドと保護メソッドが必要です。



 private +Transactional def send_money(from, to, amount) ... end
      
      







クラスメソッド


Method_added 」と「 define_method 」はクラスインスタンスメソッドでのみ機能するため、デコレータがクラス自体のメソッドで機能するように、他の何かを考え出す必要があります。



 +Memoize def self.calculate ... end
      
      







引数


Pythonの例では、デコレーターに値を渡すことができることを示しました。 メソッドのデコレータの個々のインスタンスを作成できるようにしたいと思います。



 +Retry.new(3) def post_to_facebook ... end
      
      







gem install method_decorators



github.com/michaelfairley/method_decorators



これらのすべての機能を実装し、徹底的なテストセットを追加して、gemの形ですべて展開しました。 コードをきれいにし、読みやすく、保守しやすくすると思うので、これを使用してください。



All Articles