Existe uma maneira de acessar argumentos de método em Ruby?

102

Novo em Ruby e ROR e amando isso a cada dia, então aqui está minha pergunta, já que não tenho ideia de como pesquisar no Google (e tentei :))

nós temos 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

Portanto, o que estou procurando é uma maneira de obter todos os argumentos passados ​​para o método, sem listar cada um. Como se trata de Ruby, presumo que haja uma maneira :) se fosse java, eu apenas os listaria :)

A saída seria:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Haris Krajina
fonte
1
Reha kralj svegami!
formiga
1
Acho que todas as respostas devem apontar que se "algum código" alterar os valores dos argumentos antes que o método de descoberta de argumento seja executado, ele mostrará os novos valores, não os valores que foram passados. Portanto, você deve capturá-los corretamente longe para ter certeza. Dito isso, minha method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
frase

Respostas:

159

No Ruby 1.9.2 e posterior, você pode usar o parametersmétodo em um método para obter a lista de parâmetros para aquele método. Isso retornará uma lista de pares indicando o nome do parâmetro e se ele é obrigatório.

por exemplo

Se você fizer

def foo(x, y)
end

então

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

Você pode usar a variável especial __method__para obter o nome do método atual. Portanto, dentro de um método, os nomes de seus parâmetros podem ser obtidos via

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

Você pode então exibir o nome e o valor de cada parâmetro com

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

Nota: uma vez que esta resposta foi escrita originalmente, nas versões atuais do Ruby evalnão pode mais ser chamada com um símbolo. Para resolver isso, um explícito to_sfoi adicionado ao construir a lista de nomes de parâmetros, ou seja,parameters.map { |arg| arg[1].to_s }

Mikej
fonte
4
Vou precisar de algum tempo para decifrar isso :)
Haris Krajina
3
Deixe-me saber quais bits precisam ser decifrados e eu adicionarei algumas explicações :)
mikej
3
+1 isto é realmente incrível e elegante; definitivamente a melhor resposta.
Andrew Marshall
5
Eu tentei com Ruby 1.9.3, e você tem que fazer # {eval arg.to_s} para fazê-lo funcionar, caso contrário, você obterá um TypeError: não é possível converter o símbolo em string
Javid Jamae
5
Enquanto isso, melhorei minhas habilidades e entendi esse código agora.
Haris Krajina
55

Desde o Ruby 2.1, você pode usar binding.local_variable_get para ler o valor de qualquer variável local, incluindo parâmetros de método (argumentos). Graças a isso, você pode melhorar a resposta aceita 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
fonte
é 2.1 o mais antigo?
uchuugaka
@uchuugaka Sim, este método não está disponível em <2.1.
Jakub Jirutka
Obrigado. Isso torna isso bom: logger.info method __ + "# {args.inspect}" method ( _method ) .parameters.map do | , nome | logger.info "# {name} =" + binding.local_variable_get (name) end
Martin Cleaver
Este é o caminho a seguir.
Ardee Aram
1
Também é potencialmente útil - transformar os argumentos em um hash nomeado:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
sheba
19

Uma maneira de lidar com isso é:

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
fonte
2
Funcionando e serei votado como aceito a menos que haja respostas melhores, meu único problema com isso é não quero perder a assinatura do método, alguns lá haverá o Inteli sense e eu odiaria perdê-lo.
Haris Krajina
7

Esta é uma pergunta interessante. Talvez usando local_variables ? Mas deve haver uma maneira diferente de usar eval. Estou procurando no documento do kernel

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
fonte
Isso não é tão ruim, por que essa solução maligna?
Haris Krajina
Não é ruim neste caso - usar eval () às vezes pode levar a falhas de segurança. Só acho que pode haver uma maneira melhor :) mas admito que o Google não é nosso amigo neste caso
Raffaele
Eu vou continuar com isso, a desvantagem é que você não pode fazer helper (módulo) que tomaria conta disso, já que assim que sai do método original não pode fazer evals de vars locais. Obrigado a todos pela informação.
Haris Krajina
Isso me dá "TypeError: não é possível converter Símbolo em String", a menos que eu altere para eval var.to_s. Além disso, uma advertência é que, se você definir quaisquer variáveis ​​locais antes de executar este loop, elas serão incluídas além dos parâmetros do método.
Andrew Marshall
6
Esta não é a abordagem mais elegante e segura - se você definir a variável local dentro do seu método e depois chamar local_variables, ele retornará os argumentos do método + todas as variáveis ​​locais. Isso pode causar erros quando seu código.
Aliaksei Kliuchnikau
5

Isso pode 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
fonte
1
Em vez dessa callers_nameimplementação ligeiramente hackeada , você também pode passar __method__adiante o binding.
Tom Lord
3

Você pode definir uma 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"

E use-o em seu código como:

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

Antes de prosseguir, você está passando muitos argumentos para foo. Parece que todos esses argumentos são atributos de um modelo, correto? Você realmente deveria estar passando o próprio objeto. Fim do discurso.

Você pode usar um argumento "splat". Ele coloca tudo em uma série. Seria parecido com:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Tom L
fonte
Discordo sobre isso, a assinatura do método é importante para a legibilidade e a reutilização do código. O objeto em si é bom, mas você deve criar instância em algum lugar, portanto, antes de chamar o método ou no método. Melhor no método, na minha opinião. por exemplo, criar método de usuário.
Haris Krajina
Para citar um homem mais inteligente do que eu, Bob Martin, em seu livro Clean Code, "o número ideal de argumentos para uma função é zero (niládico). Em seguida, vem um (monoadico), seguido de perto por dois (diádico). Três argumentos (triádico) deve ser evitado sempre que possível. Mais de três (poládico) requer uma justificativa muito especial - e então não deve ser usado de qualquer maneira. " Ele continua dizendo o que eu disse, muitos argumentos relacionados devem ser agrupados em uma classe e passados ​​como um objeto. É um bom livro, recomendo muito.
Tom L
Sem falar muito sobre isso, mas considere o seguinte: se você achar que precisa de mais / menos / argumentos diferentes, você terá quebrado sua API e terá que atualizar todas as chamadas para esse método. Por outro lado, se você passar um objeto, outras partes de seu aplicativo (ou consumidores de seu serviço) poderão continuar alegremente.
Tom L de
Eu concordo com seus pontos e, por exemplo, em Java, sempre reforçaria sua abordagem. Mas eu acho que com ROR é diferente e aqui está o porquê:
Haris Krajina
Eu concordo com seus pontos e, por exemplo, em Java, sempre reforçaria sua abordagem. Mas eu acho que com ROR é diferente e aqui está o porquê: Se você quiser salvar ActiveRecord em DB e tiver um método que o salva, você teria que montar o hash antes de eu passá-lo para o método save. Por exemplo de usuário, definimos primeiro, sobrenome, nome de usuário, etc. e, em seguida, passamos o hash para o método de salvamento que faria algo e salvaria. E aqui está o problema, como todo desenvolvedor sabe o que colocar no hash? É um registro ativo, então você teria que ver o esquema db em vez de montar o hash e ter muito cuidado para não perder nenhum símbolo.
Haris Krajina
2

Se você quiser alterar a assinatura do método, poderá fazer algo assim:

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

Ou:

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

Neste caso, interpolado argsou opts.valuesserá uma matriz, mas você pode joinse na vírgula. Felicidades

Simon Bagreev
fonte
2

Parece que o que essa pergunta está tentando fazer poderia ser feito com uma joia que acabei de lançar, https://github.com/ericbeland/exception_details . Ele irá listar variáveis ​​locais e valores (e variáveis ​​de instância) de exceções resgatadas. Pode valer a pena dar uma olhada...

ebeland
fonte
1
Essa é uma boa jóia, para usuários Rails eu também recomendaria better_errorsgem.
Haris Krajina
1

Se você precisar de argumentos como um Hash e não quiser poluir o corpo do método com extração complicada de parâmetros, use isto:

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

Basta adicionar esta classe ao seu projeto:

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
dólar
fonte
0

Se a função estiver dentro de alguma classe, você pode fazer algo assim:

class Car
  def drive(speed)
  end
end

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

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