代码之家  ›  专栏  ›  技术社区  ›  jameshfisher

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

  •  27
  • jameshfisher  · 技术社区  · 15 年前

    今天我要从Python的角度学习Ruby。有一件事我完全没能解决,那就是相当于装饰师。为了减少工作量,我尝试复制一个简单的python装饰器:

    #! /usr/bin/env python
    
    import math
    
    def document(f):
        def wrap(x):
            print "I am going to square", x
            f(x)
        return wrap
    
    @document
    def square(x):
        print math.pow(x, 2)
    
    square(5)
    

    运行这个可以让我:

    I am going to square 5
    25.0
    

    所以,我想创建一个函数正方形(x),但是在装饰它之前,它会提醒我它将要变成什么样。让我们把糖去掉,使其更基本:

    ...
    def square(x):
        print math.pow(x, 2)
    square = document(square)
    ...
    

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

    #! /usr/bin/env ruby
    
    def document(f)
        def wrap(x)
            puts "I am going to square", x
            f(x)
            end
        return wrap
        end
    
    def square(x)
        puts x**2
        end
    
    square = document(square)
    
    square(5)
    

    运行此命令将生成:

    ./ruby_decorate.rb:8:in `document': wrong number of arguments (0 for 1) (ArgumentError)
        from ./ruby_decorate.rb:15:in `'
    

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

    我试过很多其他的东西,但没有什么能让我走得更远。

    11 回复  |  直到 11 年前
        1
  •  13
  •   horseyguy    15 年前

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

    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 封闭。

        2
  •  11
  •   jameshfisher    15 年前

    好吧,我该回答了。我在这里专门针对那些试图重组大脑的蟒蛇。下面是一些有大量文档记录的代码,它们(大约)完成了我最初尝试的工作:

    正在修饰实例方法

    #! /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`.  <http://blog.jayfields.com/2006/05/ruby-extend-and-include.html>
      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)
    

    还是很困惑?我不会感到惊讶的,这几乎花了我一整天的时间。你应该知道的其他一些事情:

    • 首先,至关重要的是,阅读以下内容: http://www.softiesonrails.com/2007/8/15/ruby-101-methods-and-messages . 当您在Ruby中调用“foo”时,实际上您要做的是向它的所有者发送一条消息:“请调用您的方法“foo”。你不能像在Python中那样直接掌握Ruby中的函数;它们既滑又难以捉摸。你只能把它们看作洞穴墙壁上的影子;你只能通过恰好是它们名字的字符串/符号来引用它们。试着把Ruby中的每个方法调用“object.foo(args)”想象成python中的等效方法:“object”。 获取属性 (FoO)(ARGS)。
    • 停止在模块/类之外写入任何函数/方法定义。
    • 从一开始就接受这样的学习经历会使大脑融化,慢慢来。如果鲁比没有道理的话,打一堵墙,喝一杯咖啡,或者睡一夜。

    装饰类方法

    上面的代码修饰实例方法。如果你想直接在课堂上装饰方法呢?如果你读 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)
    

    玩得高兴!

        3
  •  7
  •   Oleg Shaldybin    15 年前

    类似于python的修饰符可以在Ruby中实现。我不会尝试解释和举例,因为Yehuda Katz已经在Ruby上发表了一篇关于装饰师DSL的好博客文章,所以我强烈建议阅读:

    最新消息:我有几次投票否决这一项,所以让我进一步解释。

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

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

        4
  •  4
  •   Peter    15 年前

    这是一个有点不寻常的问题,但很有趣。首先,我强烈建议您不要尝试直接将您的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
    

    我相信你会同意的。

        5
  •  3
  •   horseyguy    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
    
        6
  •  3
  •   Legion    11 年前

    用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中的decorator,您实际上是在传递函数“square”,从“wrap”中调用它。对于Ruby中的块,我传递的不是函数本身,而是一个调用函数的代码块,该代码块在“wrap”上下文中执行,其中yield语句是。

    与decorator不同,正在传递的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
    
        7
  •  2
  •   coreyward    12 年前

    迈克尔·费尔利在2012年的Railsconf上证明了这一点。代码可用 here on 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
    
        8
  •  1
  •   akuhn    15 年前

    你的猜测是对的。

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

    PS: 您的代码不在函数中定义函数,而是在同一对象上定义另一个函数(是的,这是Ruby的一个未记录的特性)

    class A
      def m
        def n
        end
      end
    end
    

    定义两者 m n 在A上

    注意: 引用函数的方法是

    A.method(:m)
    
        9
  •  1
  •   akuhn    15 年前

    好吧,又找到了我的代码,它在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
    
        10
  •  0
  •   mooware    15 年前

    我相信相应的Ruby习惯用法是Alias方法链,它被Rails大量使用。 This article 它也被认为是红宝石风格的装饰。

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

    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 并使 广场 一个别名 square_with_wrap .

    我相信Ruby1.8没有内置这个方法,所以您必须从Rails中复制它,但是1.9应该包含它。

    我的Ruby技能有点生疏,所以如果代码实际上不起作用,我很抱歉,但我确信它演示了这个概念。

        11
  •  0
  •   Alexey    14 年前

    在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
    

    尽管你需要一些自由。我已经写了 here 如何实现这种功能(当我试图在Rython中找到Ruby中缺少的东西时)。