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