Ruby中的装饰器(从Python迁移)

我今天花在从Python角度学习Ruby。 我完全没有解决的一件事是相当于装饰者。 为了削减一些东西,我试图复制一个简单的Python装饰器:

 #!  / usr / bin / env python

导入数学

 def文件(f):
     def wrap(x):
        打印“我要去广场”,x
         F(X)
    回归包裹

 @文献
 def square(x):
     print math.pow(x,2)

正方形(5)

运行这个给了我:

我要去5号广场
 25.0

所以,我想创建一个函数square(x),但是要对它进行修饰,以便在它发生之前提醒我它将会变成什么样。 让我们摆脱糖,使其更基本:

 ...
 def square(x):
     print math.pow(x,2)
 square =文件(正方形)
 ...

那么,我如何在Ruby中复制它? 这是我的第一次尝试:

 #!  / usr / bin / env ruby

 def文件(f)
     def wrap(x)
        把“我要去广场”,x
         F(X)
        结束
    回归包裹
    结束

 def square(x)
    把x ** 2
    结束

 square =文件(正方形)

正方形(5)

运行此生成:

 ./ruby_decorate.rb:8:在`document':错误的参数个数(0表示1)(ArgumentError)
    从./ruby_decorate.rb:15:in`'

我猜它是因为括号不是强制性的,它将我的“返回换行”作为“返回换行()”的尝试。 我知道在没有调用它的情况下无法引用函数。

我尝试了其他各种各样的东西,但没有什么能让我走得更远。

这是另一种方法,消除了别名方法名称之间冲突的问题(注意我使用模块进行装饰的其他解决方案也是一个很好的选择,因为它也避免了冲突):

module Documenter def document(func_name) old_method = instance_method(func_name) define_method(func_name) do |*args| puts "about to call #{func_name}(#{args.join(', ')})" old_method.bind(self).call(*args) end end end 

上面的代码是有效的,因为old_method局部变量在新的’hello’方法中保持活动,事实上define_method块是一个闭包。

好的,我尝试回答的时间到了。 我专门针对Pythoneers试图重新组织他们的大脑。 这里有一些记录严密的代码(大致)执行我最初尝试做的事情:

装饰实例方法

 #! /usr/bin/env ruby # First, understand that decoration is not 'built in'. You have to make # your class aware of the concept of decoration. Let's make a module for this. module Documenter def document(func_name) # This is the function that will DO the decoration: given a function, it'll extend it to have 'documentation' functionality. new_name_for_old_function = "#{func_name}_old".to_sym # We extend the old function by 'replacing' it - but to do that, we need to preserve the old one so we can still call it from the snazzy new function. alias_method(new_name_for_old_function, func_name) # This function, alias_method(), does what it says on the tin - allows us to call either function name to do the same thing. So now we have TWO references to the OLD crappy function. Note that alias_method is NOT a built-in function, but is a method of Class - that's one reason we're doing this from a module. define_method(func_name) do |*args| # Here we're writing a new method with the name func_name. Yes, that means we're REPLACING the old method. puts "about to call #{func_name}(#{args.join(', ')})" # ... do whatever extended functionality you want here ... send(new_name_for_old_function, *args) # This is the same as `self.send`. `self` here is an instance of your extended class. As we had TWO references to the original method, we still have one left over, so we can call it here. end end end class Squarer # Drop any idea of doing things outside of classes. Your method to decorate has to be in a class/instance rather than floating globally, because the afore-used functions alias_method and define_method are not global. extend Documenter # We have to give our class the ability to document its functions. Note we EXTEND, not INCLUDE - this gives Squarer, which is an INSTANCE of Class, the class method document() - we would use `include` if we wanted to give INSTANCES of Squarer the method `document`.  def square(x) # Define our crappy undocumented function. puts x**2 end document(:square) # this is the same as `self.document`. `self` here is the CLASS. Because we EXTENDED it, we have access to `document` from the class rather than an instance. `square()` is now jazzed up for every instance of Squarer. def cube(x) # Yes, the Squarer class has got a bit to big for its boots puts x**3 end document(:cube) end # Now you can play with squarers all day long, blissfully unaware of its ability to `document` itself. squarer = Squarer.new squarer.square(5) squarer.cube(5) 

仍然困惑? 我不会感到惊讶; 这几乎花了整整一天。 你应该知道的其他一些事情:

  • 第一件事就是CRUCIAL,请阅读: http : //www.softiesonrails.com/2007/8/15/ruby-101-methods-and-messages 。 当你在Ruby中调用’foo’时,你实际上正在做的是向其所有者发送消息:“请调用你的方法’foo’”。 你不能以Python中的方式直接掌握Ruby中的函数; 他们很滑,难以捉摸。 你只能看到它们就像洞穴墙上的阴影一样; 你只能通过恰好是他们名字的字符串/符号来引用它们。 试着想一下你在Ruby中使用的每个方法调用’object.foo(args)’等同于Python:’object。 getattribute (’foo’)(args)’。
  • 停止在模块/类之外编写任何函数/方法定义。
  • 从一开始就接受这种学习经历将会融化,并花时间。 如果Ruby没有意义,可以打一堵墙,去喝杯咖啡,或者睡个好觉。

装饰类方法

上面的代码修饰了实例方法。 如果你想直接在类上装饰方法怎么办? 如果您阅读http://www.rubyfleebie.com/understanding-class-methods-in-ruby ,您会发现创建类方法有三种方法 – 但其中只有一种方法适用于我们。

那就是匿名class << self技术”。 让我们做上面的事情,但所以我们可以调用square()和cube()而不实例化它:

 class Squarer class << self # class methods go in here extend Documenter def square(x) puts x**2 end document(:square) def cube(x) puts x**3 end document(:cube) end end Squarer.square(5) Squarer.cube(5) 

玩得开心!

类似于Python的装饰器可以在Ruby中实现。 我不会尝试解释和举例,因为Yehuda Katz已经发布了一篇关于Ruby中装饰器DSL的好博文,所以我强烈推荐阅读它:

  • Ruby中的Python装饰器
  • 源代码和测试

更新:我在这个问题上有几个投票,所以让我进一步解释。

alias_method (and alias_method_chain)与装饰器的概念不完全相同。 它只是一种在不使用inheritance的情况下重新定义方法实现的方法(因此客户端代码不会注意到差异,仍然使用相同的方法调用)。 它可能很有用。 但它也可能容易出错。 任何使用Ruby的Gettext库的人都可能注意到它的ActiveRecord集成已经被每个Rails主要升级打破了,因为别名版本一直遵循旧方法的语义。

一般来说,装饰器的目的不是改变任何给定方法的内部结构,而是仍然能够从修改后的版本中调用原始方法,而是增强函数行为。 “进入/退出”用例与alias_method_chain有点接近,只是一个简单的演示。 另一个更有用的装饰器可能是@login_required ,它检查授权,只在授权成功时运行函数,或@trace(arg1, arg2, arg3) ,它们可以执行一组跟踪程序(并被调用)用不同的方法装饰不同的参数)。

这是一个有点不寻常的问题,但有趣。 我首先强烈建议您不要尝试直接将Python知识转移到Ruby上 – 最好学习Ruby的习惯并直接应用它们,而不是尝试直接转移Python。 我已经使用了两种语言,并且在遵循自己的规则和约定时它们都是最好的。

说了这么多,这里有一些你可以使用的漂亮代码。

 def with_document func_name, *args puts "about to call #{func_name}(#{args.to_s[1...-1]})" method(func_name).call *args end def square x puts x**2 end def multiply a, b puts a*b end with_document :square, 5 with_document :multiply, 5, 3 

这会产生

 about to call square(5) 25 about to call multiply(5, 3) 15 

我相信你会同意这份工作。

到目前为止,IMO mooware有最好的答案,它是最干净,最简单和最惯用的。 然而,他正在使用’alias_method_chain’,它是Rails的一部分,而不是纯Ruby。 这是使用纯Ruby的重写:

 class Foo def square(x) puts x**2 end alias_method :orig_square, :square def square(x) puts "I am going to square #{x}" orig_square(x) end end 

您也可以使用模块来完成相同的事情:

 module Decorator def square(x) puts "I am going to square #{x}" super end end class Foo def square(x) puts x**2 end end # let's create an instance foo = Foo.new # let's decorate the 'square' method on the instance foo.extend Decorator # let's invoke the new decorated method foo.square(5) #=> "I am going to square 5" #=> 25 

Michael Fairley在2012年的RailsConf上展示了这一点.Github上提供了代码。 简单用法示例:

 class Math extend MethodDecorators +Memoized def fib(n) if n <= 1 1 else fib(n - 1) * fib(n - 2) end end end # or using an instance of a Decorator to pass options class ExternalService extend MethodDecorators +Retry.new(3) def request ... end end 

你可以用Python中的装饰器实现什么,你可以用Ruby实现块。 (我无法相信这个页面上有多少答案,没有一个屈服声明!)

 def wrap(x) puts "I am going to square #{x}" yield x end def square(x) x**2 end >> wrap(2) { |x| square(x) } => I am going to square 2 => 4 

这个概念很相似。 使用Python中的装饰器,你实际上是在“wrap”中传递函数“square”。 使用Ruby中的块,我不是传递函数本身,而是传递函数调用的代码块,并且该代码块在“wrap”的上下文中执行,其中yield语句是。

与装饰器不同,传递的Ruby块不需要函数作为它的一部分。 以上可能只是:

 def wrap(x) puts "I am going to square #{x}" yield x end >> wrap(4) { |x| x**2 } => I am going to square 4 => 16 

你的猜测是正确的。

您最好使用别名将原始方法绑定到另一个名称,然后定义新方法以打印某些内容并调用旧方法。 如果你需要重复这样做,你可以为任何方法创建一个方法(我有一个例子,但现在找不到它)。

PS:你的代码没有在函数中定义函数,而是在同一个对象上定义另一个函数(是的,这是Ruby的一个非文档特性)

 class A def m def n end end end 

在A上定义mn

注意:引用函数的方法是

 A.method(:m) 

好的,再次发现我的代码在Ruby中执行装饰器。 它使用别名将原始方法绑定到另一个名称,然后定义新的方法以打印某些内容并调用旧方法。 所有这些都是使用eval完成的,因此它可以像Python中的装饰器一样重用。

 module Document def document(symbol) self.send :class_eval, """ alias :#{symbol}_old :#{symbol} def #{symbol} *args puts 'going to #{symbol} '+args.join(', ') #{symbol}_old *args end""" end end class A extend Document def square(n) puts n * n end def multiply(a,b) puts a * b end document :square document :multiply end a = A.new a.square 5 a.multiply 3,4 

编辑:这里与块相同(没有字符串操作痛苦)

 module Document def document(symbol) self.class_eval do symbol_old = "#{symbol}_old".to_sym alias_method symbol_old, symbol define_method symbol do |*args| puts "going to #{symbol} "+args.join(', ') self.send symbol_old, *args end end end end 

我相信相应的Ruby习惯用法是别名方法链,它被Rails大量使用。 本文还将其视为Ruby风格的装饰器。

对于您的示例,它应如下所示:

 class Foo def square(x) puts x**2 end def square_with_wrap(x) puts "I am going to square", x square_without_wrap(x) end alias_method_chain :square, :wrap end 

alias_method_chain调用将square重命名为square_without_wrap ,并使squaresquare_with_wrap的别名。

我相信Ruby 1.8没有内置的这种方法,所以你必须从Rails复制它,但1.9应该包含它。

我的Ruby-Skills有点生疏,所以如果代码实际上没有用,我很抱歉,但我确信它会certificate这个概念。

在Ruby中,您可以模仿Python的装饰器语法,如下所示:

 def document decorate_next_def {|name, to_decorate| print "I am going to square", x to_decorate } end document def square(x) print math.pow(x, 2) end 

虽然你需要一些lib。 我在这里写了如何实现这样的function(当我试图在Rython中找到Ruby中缺少的东西时)。