section - Decoradores en Ruby(emigrando de Python)
wbr tag in html5 (11)
Paso hoy aprendiendo Ruby desde una perspectiva de Python. Una cosa con la que no he podido lidiar es con un equivalente de decoradores. Para reducir las cosas, estoy tratando de replicar un decorador trivial de 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)
Ejecutar esto me da:
I am going to square 5 25.0
Entonces, quiero crear una función cuadrada (x), pero decorarla para que me avise de lo que va a cuadrar antes de que lo haga. Vamos a deshacernos del azúcar para hacerlo más básico:
... def square(x): print math.pow(x, 2) square = document(square) ...
Entonces, ¿cómo puedo replicar esto en Ruby? Aquí está mi primer intento:
#! /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)
Ejecutando esto se genera:
./ruby_decorate.rb:8:in `document'': wrong number of arguments (0 for 1) (ArgumentError) from ./ruby_decorate.rb:15:in `''
Lo cual supongo, porque los paréntesis no son obligatorios y está tomando mi "ajuste de devolución" como un intento de "devolver ajuste ()". No conozco ninguna forma de referirme a una función sin llamarla.
He intentado varias otras cosas, pero nada me lleva lejos.
Aquí hay otro enfoque que elimina el problema con los conflictos entre nombres de métodos con alias (NOTA: mi otra solución que usa módulos para la decoración también es una buena alternativa, ya que también evita conflictos):
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
El código anterior funciona porque la variable local old_method
se mantiene viva en el nuevo método ''hola'' por el hecho de que el bloque define_method
es un cierre.
Bien, encontré mi código otra vez que hace decoradores en Ruby. Utiliza un alias para enlazar el método original con otro nombre, y luego define el nuevo para imprimir algo y llamar al antiguo. Todo esto se hace utilizando eval, de manera que se puede reutilizar como decoradores en 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
Edición: aquí lo mismo con un bloque (sin dolor de manipulación de cadena)
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
Creo que el idioma de Ruby correspondiente sería la cadena del método alias, que es muy utilizada por Rails. Este artículo también lo considera como el decorador de estilo rubí.
Para tu ejemplo debería verse así:
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
La llamada alias_method_chain
cambia el nombre de square
a square_without_wrap
y hace square
un alias para square_with_wrap
.
Creo que Ruby 1.8 no tiene este método incorporado, por lo que tendrías que copiarlo desde Rails, pero 1.9 debería incluirlo.
Mis habilidades de rubí se han oxidado un poco, así que lo siento si el código no funciona, pero estoy seguro de que demuestra el concepto.
En Ruby puedes imitar la sintaxis de Python para los decoradores de la siguiente manera:
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
Aunque necesitas algo de lib para eso. He escrito here cómo implementar dicha funcionalidad (cuando intentaba encontrar algo en Rython que falta en Ruby).
Esta es una pregunta un poco inusual, pero interesante. Primero, recomiendo encarecidamente que no intentes transferir directamente tus conocimientos de Python a Ruby; es mejor aprender los idiomas de Ruby y aplicarlos directamente, en lugar de intentar transferir Python directamente. He usado mucho los dos idiomas, y ambos son mejores cuando siguen sus propias reglas y convenciones.
Habiendo dicho todo esto, aquí hay un código ingenioso que puedes usar.
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
esto produce
about to call square(5)
25
about to call multiply(5, 3)
15
que estoy seguro de que estará de acuerdo hace el trabajo.
IMO mooware tiene la mejor respuesta hasta el momento y es la más limpia, la más simple y la más idiomática. Sin embargo, está haciendo uso de ''alias_method_chain'', que forma parte de Rails, y no de Ruby puro. Aquí hay una reescritura usando Ruby puro:
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
También puedes lograr lo mismo usando módulos en su lugar:
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
Lo que podrías lograr con los decoradores en Python, lo lograrás con los bloques en Ruby. (No puedo creer cuántas respuestas hay en esta página, ¡sin una sola declaración de rendimiento!)
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
El concepto es similar. Con el decorador en Python, esencialmente estás pasando la función "cuadrada" para ser llamada desde dentro de "wrap". Con el bloque en Ruby, no estoy pasando la función en sí, sino un bloque de código dentro del cual se invoca la función, y ese bloque de código se ejecuta dentro del contexto de "ajuste", donde está la declaración de rendimiento.
A diferencia de los decoradores, el bloque Ruby que se pasa no necesita una función para ser parte de él. Lo anterior podría haber sido simplemente:
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
Los decoradores similares a los pitones se pueden implementar en Ruby. No intentaré explicar y dar ejemplos, porque Yehuda Katz ya ha publicado una buena publicación en el blog sobre decoradores DSL en Ruby, así que recomiendo leerlo:
ACTUALIZACIÓN: Tengo un par de bajas de votos en este caso, así que déjame explicarte más.
alias_method (and alias_method_chain)
NO es exactamente el mismo concepto que un decorador. Es solo una forma de redefinir la implementación del método sin usar la herencia (de modo que el código del cliente no notará una diferencia, aún utilizando la misma llamada de método). Podría ser útil. Pero también podría ser propenso a errores. Cualquiera que haya usado la biblioteca Gettext para Ruby probablemente notó que su integración ActiveRecord se ha roto con cada actualización importante de Rails, porque la versión con alias ha seguido la semántica de un método antiguo.
El propósito de un decorador en general NO es cambiar los elementos internos de ningún método dado y aún ser capaz de llamar al original desde una versión modificada, sino mejorar el comportamiento de la función. El caso de uso de "entrada / salida", que es algo alias_method_chain
a alias_method_chain
, es solo una simple demostración. Otra clase más útil de decorador podría ser @login_required
, que verifica la autorización, y solo ejecuta la función si la autorización fue exitosa, o @trace(arg1, arg2, arg3)
, que podría realizar un conjunto de procedimientos de rastreo (y ser llamado Con diferentes argumentos para diferentes métodos de decoración).
Michael Fairley demostró esto en RailsConf 2012. El código está disponible aquí en Github . Ejemplos de uso simple:
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
Ok, es hora de mi intento de respuesta. Estoy apuntando aquí específicamente a los pitones que intentan reorganizar sus cerebros. Aquí hay un código muy documentado que (aproximadamente) hace lo que originalmente intentaba hacer:
Decorar métodos de instancia.
#! /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)
¿Sigo confundido? No me sorprendería; Esto me ha llevado casi un día entero. Algunas otras cosas que debes saber:
- Lo primero, que es CRUCIAL, es leer esto: http://www.softiesonrails.com/2007/8/15/ruby-101-methods-and-messages . Cuando llamas ''foo'' en Ruby, lo que estás haciendo en realidad es enviar un mensaje a su propietario: "llama a tu método ''foo''". Simplemente no puede obtener una retención directa de las funciones en Ruby de la forma que lo hace en Python; son resbaladizos y esquivos Solo puedes verlos como si fueran sombras en la pared de una cueva; solo puede hacer referencia a ellos a través de cadenas / símbolos que resultan ser su nombre. Intenta y piensa en cada método llamado ''object.foo (args)'' que haces en Ruby como el equivalente de esto en Python: ''object. getattribute (''foo'') (args) ''.
- Deja de escribir cualquier definición de función / método fuera de los módulos / clases.
- Acepte desde el principio que esta experiencia de aprendizaje se derretirá el cerebro y tómese su tiempo. Si Ruby no tiene sentido, golpea una pared, prepara una taza de café o duerme una noche.
Decorar los métodos de clase.
El código anterior decora los métodos de instancia. ¿Qué pasa si quieres decorar métodos directamente en la clase? Si lees http://www.rubyfleebie.com/understanding-class-methods-in-ruby , encuentras que hay tres métodos para crear métodos de clase, pero solo uno de ellos funciona para nosotros aquí.
Esa es la class << self
anónima class << self
. Hagamos lo anterior pero así podemos llamar a square () y cube () sin instanciarlo:
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)
¡Que te diviertas!
Tu conjetura es correcta.
Es mejor usar el alias para enlazar el método original con otro nombre, y luego definir el nuevo para imprimir algo y llamar al antiguo. Si necesita hacer esto repetidamente, puede crear un método que lo haga para cualquier método (tuve un ejemplo una vez, pero ahora no puedo encontrarlo).
PD: tu código no define una función dentro de una función sino otra función en el mismo objeto (sí, esta es una característica de no documentar de Ruby)
class A
def m
def n
end
end
end
define tanto m
como n
en A.
NB: la forma de referirse a una función sería
A.method(:m)