Подтвердить что ты не робот

Декораторы в Ruby (мигрирующие с Python)

Я трачу сегодня на изучение Ruby с точки зрения Python. Одна вещь, с которой я полностью не справился, - это эквивалент декораторов. Чтобы избавиться от вещей, я пытаюсь воспроизвести тривиальный декоратор 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 `'

Я думаю, это потому, что скобки не являются обязательными, и он берет мой "обратный перенос" как попытку "return wrap()". Я не знаю, как обращаться к функции, не вызывая ее.

Я пробовал разные вещи, но меня ничего не дало.

4b9b3361

Ответ 1

Вот еще один подход, который устраняет проблему с конфликтами между именами псевдонимов (обратите внимание, что мое другое решение с использованием модулей для декорирования является хорошей альтернативой, так как оно также позволяет избежать конфликтов):

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 сохраняется в новом методе "привет" по факту закрытия блока define_method.

Ответ 2

Хорошо, время для моей попытки ответить. Я нацелился здесь конкретно на 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 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 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)

Все еще запутался? Я не удивлюсь; это заняло у меня почти целый ДЕНЬ. Некоторые другие вещи, которые вы должны знать:

  • Первое, что является CRUCIAL, заключается в следующем: http://www.softiesonrails.com/2007/8/15/ruby-101-methods-and-messages. Когда вы вызываете "foo" в Ruby, то, что вы на самом деле делаете, отправляет сообщение своему владельцу: "Пожалуйста, назовите ваш метод" foo ". Вы просто не можете прямо контролировать функции в Ruby так, как вы можете в Python; они скользкие и неуловимые. Вы можете видеть их только как тени на стене пещеры; вы можете ссылаться только на них через строки/символы, которые являются их именем. Попробуйте и подумайте о каждом вызове метода" object.foo(args)", который вы делаете в Ruby как эквивалент этого в объекте Python:. getattribute ('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

Python-подобные декораторы могут быть реализованы в Ruby. Я не буду пытаться объяснять и приводить примеры, потому что Иегуда Кац уже опубликовал хорошее сообщение в блоге о декораторах DSL в Ruby, поэтому я настоятельно рекомендую прочитать его:

ОБНОВЛЕНИЕ: У меня есть несколько пропусков на голосование по этому поводу, поэтому позвольте мне объяснить далее.

alias_method (and alias_method_chain) НЕ является тем же понятием, что и декоратор. Это всего лишь способ переопределить реализацию метода без использования наследования (поэтому клиентский код не заметит разницы, все еще используя тот же вызов метода). Это может быть полезно. Но и это может быть подвержено ошибкам. Любой, кто использовал библиотеку Gettext для Ruby, вероятно, заметил, что ее интеграция с ActiveRecord была нарушена при каждом обновлении Rails, потому что aliased-версия следит за семантикой старого метода.

Целью декоратора в общем случае является НЕ изменять внутренности любого данного метода и по-прежнему иметь возможность вызывать исходный из модифицированной версии, но для улучшения поведения функции. Вариант использования "вход/выход", который несколько близок к alias_method_chain, является лишь простой демонстрацией. Другим, более полезным видом декоратора может быть @login_required, который проверяет авторизацию и запускает только эту функцию, если авторизация прошла успешно, или @trace(arg1, arg2, arg3), которая может выполнять набор процедур трассировки (и вызываться с разными аргументами для разных методы оформления).

Ответ 4

Это немного необычный вопрос, но интересный. Сначала я настоятельно рекомендую вам не пытаться напрямую передавать знания 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

У 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 create an instance
foo = Foo.new

# let decorate the 'square' method on the instance
foo.extend Decorator

# let invoke the new decorated method
foo.square(5) #=> "I am going to square 5"
              #=> 25

Ответ 6

Майкл Фэрли продемонстрировал это на 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

Ответ 7

Что вы можете достичь с помощью декораторов в 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". С блоком в 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

Ответ 8

Ваша догадка правильная.

Лучше всего использовать псевдоним, чтобы привязать исходный метод к другому имени, а затем определить новый для печати и вызвать старый. Если вам нужно сделать это несколько раз, вы можете сделать метод, который делает это для любого метода (у меня был пример один раз, но он не может найти его сейчас).

PS: ваш код не определяет функцию внутри функции, а другую функцию на одном и том же объекте (да, это недокументированная функция Ruby)

class A
  def m
    def n
    end
  end
end

определяет как m, так и n на A.

NB: способ ссылки на функцию будет

A.method(:m)

Ответ 9

Хорошо, снова нашел мой код, который делает декораторы в 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

Я считаю, что соответствующая идиома 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 должен включать его.

My Ruby-Skills получили немного ржавый, поэтому я сожалею, если код на самом деле не работает, но я уверен, что он демонстрирует концепцию.

Ответ 11

В 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. Я написал здесь, как реализовать такую ​​функциональность (когда я пытался найти там что-то в Rython, отсутствующее в Ruby).