繁体   English   中英

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

[英]Decorators in Ruby (migrating from Python)

我今天从 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

所以,我想创建一个函数 square(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 `'

我猜这是因为括号不是强制性的,它把我的“返回包装”作为“返回包装()”的尝试。 我知道不调用函数就无法引用它。

我尝试了其他各种事情,但没有什么能让我走得更远。

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

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

上面的代码有效,因为由于define_method块是一个闭包, old_method局部变量在新的“hello”方法中保持活动状态。

好的,是时候尝试回答了。 我在这里专门针对试图重组大脑的 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`.  <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)'。 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)

玩得开心!

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

更新:我对这个有几个投票否决,所以让我进一步解释一下。

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

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

您可以使用 Python 中的装饰器实现的功能,您可以使用 Ruby 中的块实现。 (我不敢相信这个页面上有多少答案,没有一个 yield 声明!)

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

这是一个有点不寻常的问题,但很有趣。 我首先强烈建议您不要尝试将您的 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 有最好的答案,它是最干净、最简单和最惯用的。 然而,他正在使用作为 Rails 一部分的“alias_method_chain”,而不是纯 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 在 RailsConf 2012 上演示了这一点。代码在 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

你的猜测是对的。

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

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并使square成为square_with_wrap的别名。

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

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

在 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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM