¿Hay alguna forma de acceder a los argumentos del método en Ruby?

102

Nuevo en Ruby y ROR y me encanta cada día, así que aquí está mi pregunta ya que no tengo idea de cómo buscarlo en Google (y lo he intentado :))

tenemos método

def foo(first_name, last_name, age, sex, is_plumber)
    # some code
    # error happens here
    logger.error "Method has failed, here are all method arguments #{SOMETHING}"    
end

Entonces, lo que estoy buscando es la forma de pasar todos los argumentos al método, sin enumerar cada uno. Como esto es Ruby, supongo que hay una manera :) si fuera Java, simplemente los enumeraría :)

La salida sería:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Haris Krajina
fuente
1
Reha kralj svegami!
hormiga
1
Creo que todas las respuestas deben señalar que si "algún código" cambia los valores de los argumentos antes de que se ejecute el método de descubrimiento de argumentos, mostrará los nuevos valores, no los valores que se pasaron. Por lo tanto, debe tomarlos correctamente lejos para estar seguro. Dicho esto, mi method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
frase

Respuestas:

159

En Ruby 1.9.2 y versiones posteriores, puede usar el parametersmétodo en un método para obtener la lista de parámetros para ese método. Esto devolverá una lista de pares indicando el nombre del parámetro y si es necesario.

p.ej

Si lo haces

def foo(x, y)
end

luego

method(:foo).parameters # => [[:req, :x], [:req, :y]]

Puede utilizar la variable especial __method__para obtener el nombre del método actual. Entonces, dentro de un método, los nombres de sus parámetros se pueden obtener a través de

args = method(__method__).parameters.map { |arg| arg[1].to_s }

A continuación, puede mostrar el nombre y el valor de cada parámetro con

logger.error "Method failed with " + args.map { |arg| "#{arg} = #{eval arg}" }.join(', ')

Nota: dado que esta respuesta se escribió originalmente, en las versiones actuales de Ruby evalya no se puede llamar con un símbolo. Para abordar esto, se to_sha agregado un explícito al construir la lista de nombres de parámetros, es decirparameters.map { |arg| arg[1].to_s }

mikej
fuente
4
Voy a necesitar algo de tiempo para descifrar esto :)
Haris Krajina
3
Déjame saber qué bits necesitan descifrar y
agregaré
3
+1 esto es realmente impresionante y elegante; definitivamente la mejor respuesta.
Andrew Marshall
5
Probé con Ruby 1.9.3, y tienes que hacer # {eval arg.to_s} para que funcione, de lo contrario obtienes un TypeError: no se puede convertir el símbolo en una cadena
Javid Jamae
5
Mientras tanto, mejoré mis habilidades y entiendo este código ahora.
Haris Krajina
55

Desde Ruby 2.1, puede usar binding.local_variable_get para leer el valor de cualquier variable local, incluidos los parámetros del método (argumentos). Gracias a eso puedes mejorar la respuesta aceptada para evitarmal eval.

def foo(x, y)
  method(__method__).parameters.map do |_, name|
    binding.local_variable_get(name)
  end
end

foo(1, 2)  # => 1, 2
Jakub Jirutka
fuente
es 2.1 el más temprano?
uchuugaka
@uchuugaka Sí, este método no está disponible en <2.1.
Jakub Jirutka
Gracias. Eso hace que esto sea agradable: método logger.info __ + método "# {args.inspect}" ( _method ) .parameters.map do | , nombre | logger.info "# {name} =" + binding.local_variable_get (name) end
Martin Cleaver
Este es el camino a seguir.
Ardee Aram
1
También potencialmente útil - la transformación de los argumentos para un hash llamado:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
seba
19

Una forma de manejar esto es:

def foo(*args)
    first_name, last_name, age, sex, is_plumber = *args
    # some code
    # error happens here
    logger.error "Method has failed, here are all method arguments #{args.inspect}"    
end
Arun Kumar Arjunan
fuente
2
Funciona y se votará como aceptado a menos que haya mejores respuestas, mi único problema con esto es que no quiero perder la firma del método, algunos habrá sentido Inteli y odiaría perderlo.
Haris Krajina
7

Esta es una pregunta interesante. ¿Quizás usando local_variables ? Pero debe haber una forma distinta de usar eval. Estoy buscando en Kernel doc

class Test
  def method(first, last)
    local_variables.each do |var|
      puts eval var.to_s
    end
  end
end

Test.new().method("aaa", 1) # outputs "aaa", 1
Raffaele
fuente
Esto no es tan malo, ¿por qué esta mala solución?
Haris Krajina
No está mal en este caso: el uso de eval () a veces puede provocar agujeros de seguridad. Solo creo que puede haber una manera mejor :) pero admito que Google no es nuestro amigo en este caso
Raffaele
Voy a seguir con esto, la desventaja es que no puede hacer helper (módulo) que se encargaría de esto, ya que tan pronto como deja el método original no puede hacer evaluaciones de vars locales. Gracias a todos por la información.
Haris Krajina
Esto me da "TypeError: no se puede convertir Symbol en String" a menos que lo cambie a eval var.to_s. Además, una advertencia a esto es que si define cualquier variable local antes de ejecutar este ciclo, se incluirá además de los parámetros del método.
Andrew Marshall
6
Este no es el enfoque más elegante y seguro: si define la variable local dentro de su método y luego llama local_variables, devolverá los argumentos del método + todas las variables locales. Esto puede provocar errores cuando su código.
Aliaksei Kliuchnikau
5

Esto puede ser útil ...

  def foo(x, y)
    args(binding)
  end

  def args(callers_binding)
    callers_name = caller[0][/`.*'/][1..-2]
    parameters = method(callers_name).parameters
    parameters.map { |_, arg_name|
      callers_binding.local_variable_get(arg_name)
    }    
  end
Jon Jagger
fuente
1
En lugar de esta callers_nameimplementación ligeramente pirateada , también podría pasar __method__junto con el binding.
Tom Lord
3

Puede definir una constante como:

ARGS_TO_HASH = "method(__method__).parameters.map { |arg| arg[1].to_s }.map { |arg| { arg.to_sym => eval(arg) } }.reduce Hash.new, :merge"

Y utilícelo en su código como:

args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
Al Johri
fuente
2

Antes de continuar, estás pasando demasiados argumentos a foo. Parece que todos esos argumentos son atributos de un modelo, ¿correcto? Realmente debería pasar el objeto en sí. Fin del discurso.

Podría utilizar un argumento "splat". Empuja todo en una matriz. Se vería así:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Tom L
fuente
No estoy de acuerdo con esto, la firma del método es importante para la legibilidad y reutilización del código. El objeto en sí está bien, pero debe crear una instancia en algún lugar, así que antes de llamar al método o en el método. Mejor en método en mi opinión. por ejemplo, crear un método de usuario.
Haris Krajina
Para citar a un hombre más inteligente que yo, Bob Martin, en su libro Código limpio, "el número ideal de argumentos para una función es cero (niladic). Luego viene uno (monoádico), seguido de cerca por dos (diádico). Tres argumentos (triádica) debe evitarse siempre que sea posible. Más de tres (poliádica) requiere una justificación muy especial, y luego no debe usarse de todos modos ". Continúa diciendo lo que dije, muchos argumentos relacionados deben estar envueltos en una clase y pasados ​​como un objeto. Es un buen libro, lo recomiendo mucho.
Tom L
No para poner un punto demasiado fino, pero considere esto: si encuentra que necesita más / menos / diferentes argumentos, habrá roto su API y tendrá que actualizar cada llamada a ese método. Por otro lado, si pasa un objeto, otras partes de su aplicación (o consumidores de su servicio) pueden avanzar alegremente.
Tom L
Estoy de acuerdo con sus puntos y, por ejemplo, en Java siempre haría cumplir su enfoque. Pero creo que con ROR es diferente y he aquí por qué:
Haris Krajina
Estoy de acuerdo con sus puntos y, por ejemplo, en Java siempre haría cumplir su enfoque. Pero creo que con ROR es diferente y esta es la razón: si desea guardar ActiveRecord en DB y tiene un método que lo guarda, tendría que ensamblar hash antes de que lo pase para guardar el método. Por ejemplo de usuario, configuramos el nombre, apellido, nombre de usuario, etc. y luego pasamos el hash para guardar el método que haría algo y lo guardaría. Y aquí está el problema: ¿cómo sabe cada desarrollador qué poner en hash? Es un registro activo, por lo que tendría que ir a ver el esquema de la base de datos en lugar de ensamblar el hash, y tenga mucho cuidado de no perder ningún símbolo.
Haris Krajina
2

Si cambia la firma del método, puede hacer algo como esto:

def foo(*args)
  # some code
  # error happens here
  logger.error "Method has failed, here are all method arguments #{args}"    
end

O:

def foo(opts={})
  # some code
  # error happens here
  logger.error "Method has failed, here are all method arguments #{opts.values}"    
end

En este caso, interpolado args o opts.valuesserá una matriz, pero puede joinhacerlo si está en coma. Salud

Simon Bagreev
fuente
2

Parece que lo que esta pregunta está tratando de lograr podría hacerse con una gema que acabo de lanzar, https://github.com/ericbeland/exception_details . Enumerará las variables y valores locales (y las variables de instancia) de las excepciones rescatadas. Vale la pena echarle un vistazo ...

ebeland
fuente
1
Esa es una buena joya, para los usuarios de Rails también recomendaría better_errorsgem.
Haris Krajina
1

Si necesita argumentos como Hash y no desea contaminar el cuerpo del método con una extracción complicada de parámetros, use esto:

def mymethod(firstarg, kw_arg1:, kw_arg2: :default)
  args = MethodArguments.(binding) # All arguments are in `args` hash now
  ...
end

Simplemente agregue esta clase a su proyecto:

class MethodArguments
  def self.call(ext_binding)
    raise ArgumentError, "Binding expected, #{ext_binding.class.name} given" unless ext_binding.is_a?(Binding)
    method_name = ext_binding.eval("__method__")
    ext_binding.receiver.method(method_name).parameters.map do |_, name|
      [name, ext_binding.local_variable_get(name)]
    end.to_h
  end
end
tras verde
fuente
0

Si la función está dentro de alguna clase, puede hacer algo como esto:

class Car
  def drive(speed)
  end
end

car = Car.new
method = car.method(:drive)

p method.parameters #=> [[:req, :speed]] 
Nikhil Wagh
fuente