Existe-t-il un moyen d'accéder aux arguments de méthode dans Ruby?

102

Nouveau sur Ruby et ROR et j'adore ça chaque jour, alors voici ma question car je ne sais pas comment le google (et j'ai essayé :))

nous avons la méthode

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

Donc ce que je cherche un moyen d'obtenir tous les arguments passés à la méthode, sans énumérer chacun d'eux. Puisqu'il s'agit de Ruby, je suppose qu'il y a un moyen :) si c'était java, je les énumérerais simplement :)

La sortie serait:

Method has failed, here are all method arguments {"Mario", "Super", 40, true, true}
Haris Krajina
la source
1
Reha kralj svegami!
fourmi
1
Je pense que toutes les réponses devraient souligner que si "un code" modifie les valeurs des arguments avant que la méthode de découverte d'arguments ne soit exécutée, il affichera les nouvelles valeurs, pas les valeurs qui ont été transmises. Vous devez donc les saisir correctement loin pour être sûr. Cela dit, mon one-liner préféré pour cela (avec le crédit accordé aux réponses précédentes) est:method(__method__).parameters.map { |_, v| [v, binding.local_variable_get(v)] }
Brian Deterling

Réponses:

159

Dans Ruby 1.9.2 et versions ultérieures, vous pouvez utiliser la parametersméthode sur une méthode pour obtenir la liste des paramètres de cette méthode. Cela renverra une liste de paires indiquant le nom du paramètre et s'il est requis.

par exemple

Si tu fais

def foo(x, y)
end

puis

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

Vous pouvez utiliser la variable spéciale __method__pour obtenir le nom de la méthode actuelle. Ainsi, dans une méthode, les noms de ses paramètres peuvent être obtenus via

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

Vous pouvez alors afficher le nom et la valeur de chaque paramètre avec

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

Remarque: depuis que cette réponse a été écrite à l'origine, dans les versions actuelles de Ruby evalne peut plus être appelée avec un symbole. Pour résoudre ce problème, un explicite to_sa été ajouté lors de la construction de la liste des noms de paramètres ieparameters.map { |arg| arg[1].to_s }

Mikej
la source
4
Je vais avoir besoin d'un peu de temps pour déchiffrer ça :)
Haris Krajina
3
Faites-moi savoir quels bits doivent être déchiffrés et j'ajouterai quelques explications :)
mikej
3
+1 c'est vraiment génial et élégant; certainement la meilleure réponse.
Andrew Marshall
5
J'ai essayé avec Ruby 1.9.3, et vous devez faire # {eval arg.to_s} pour le faire fonctionner, sinon vous obtenez un TypeError: impossible de convertir Symbol en String
Javid Jamae
5
En attendant, je me suis amélioré et mes compétences et comprendre ce code maintenant.
Haris Krajina
55

Depuis Ruby 2.1, vous pouvez utiliser binding.local_variable_get pour lire la valeur de n'importe quelle variable locale, y compris les paramètres de méthode (arguments). Grâce à cela, vous pouvez améliorer la réponse acceptée pour évitermal 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
la source
est 2.1 le plus ancien?
uchuugaka
@uchuugaka Oui, cette méthode n'est pas disponible dans <2.1.
Jakub Jirutka
Merci. Cela rend ce joli: méthode logger.info __ + méthode "# {args.inspect}" ( _method ) .parameters.map do | , nom | logger.info "# {name} =" + binding.local_variable_get (name) end
Martin Cleaver
C'est la voie à suivre.
Ardee Aram
1
Également potentiellement utile - transformer les arguments en un hachage nommé:Hash[method(__method__).parameters.map.collect { |_, name| [name, binding.local_variable_get(name)] }]
sheba
19

Une façon de gérer cela est:

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
la source
2
Travailler et sera voté comme accepté à moins qu'il n'y ait de meilleures réponses, mon seul problème avec cela est que je ne veux pas perdre la signature de la méthode, certains là-bas auront un sens Inteli et je détesterais la perdre.
Haris Krajina le
7

C'est une question intéressante. Peut-être en utilisant local_variables ? Mais il doit y avoir un autre moyen que d'utiliser eval. Je cherche dans la documentation du noyau

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
la source
Ce n'est pas si mal, pourquoi cette mauvaise solution?
Haris Krajina
Ce n'est pas mal dans ce cas - l'utilisation de eval () peut parfois conduire à des failles de sécurité. Je pense juste qu'il y a peut-être un meilleur moyen :) mais j'avoue que Google n'est pas notre ami dans ce cas
Raffaele
Je vais aller avec ceci, l'inconvénient est que vous ne pouvez pas faire d'aide (module) qui s'occuperait de cela, car dès qu'il quitte la méthode originale, il ne peut pas faire d'évaluations des variables locales. Merci à tous pour les informations.
Haris Krajina
Cela me donne "TypeError: impossible de convertir le symbole en chaîne" à moins que je ne le change en eval var.to_s. De plus, une mise en garde à ceci est que si vous définissez des variables locales avant d' exécuter cette boucle, elles seront incluses en plus des paramètres de méthode.
Andrew Marshall
6
Ce n'est pas l'approche la plus élégante et la plus sécurisée - si vous définissez une variable locale dans votre méthode puis appelez local_variables, elle retournera les arguments de méthode + toutes les variables locales. Cela peut provoquer des erreurs lorsque votre code.
Aliaksei Kliuchnikau
5

Cela peut être utile ...

  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
la source
1
Plutôt que cette callers_nameimplémentation légèrement piratée , vous pouvez également transmettre le __method__fichier binding.
Tom Lord
3

Vous pouvez définir une constante telle que:

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

Et utilisez-le dans votre code comme:

args = eval(ARGS_TO_HASH)
another_method_that_takes_the_same_arguments(**args)
Al Johri
la source
2

Avant d'aller plus loin, vous passez trop d'arguments dans foo. Il semble que tous ces arguments sont des attributs sur un modèle, n'est-ce pas? Vous devriez vraiment passer l'objet lui-même. Fin du discours.

Vous pouvez utiliser un argument "splat". Il met tout dans un tableau. Cela ressemblerait à:

def foo(*bar)
  ...
  log.error "Error with arguments #{bar.joins(', ')}"
end
Tom L
la source
En désaccord sur ce point, la signature de la méthode est importante pour la lisibilité et la réutilisation du code. L'objet lui-même va bien, mais vous devez créer une instance quelque part, donc avant d'appeler la méthode ou dans la méthode. Mieux en méthode à mon avis. par exemple, créer une méthode utilisateur.
Haris Krajina
Pour citer un homme plus intelligent que moi, Bob Martin, dans son livre Clean Code, "le nombre idéal d'arguments pour une fonction est zéro (niladique). Vient ensuite un (monoadique), suivi de près par deux (dyadique). Trois arguments (triadique) doit être évité dans la mesure du possible. Plus de trois (polyadique) nécessitent une justification très spéciale - et ne doivent donc pas être utilisés de toute façon. " Il poursuit en disant ce que j'ai dit, de nombreux arguments liés doivent être enveloppés dans une classe et passés en tant qu'objet. C'est un bon livre, je le recommande vivement.
Tom L
Il ne faut pas trop insister là-dessus, mais considérez ceci: si vous trouvez que vous avez besoin de plus / moins / d'arguments différents, vous aurez cassé votre API et devrez mettre à jour chaque appel à cette méthode. D'un autre côté, si vous passez un objet, d'autres parties de votre application (ou les consommateurs de votre service) peuvent marcher joyeusement.
Tom L
Je suis d'accord avec vos points et par exemple en Java, je ferais toujours respecter votre approche. Mais je pense que ROR est différent et voici pourquoi:
Haris Krajina
Je suis d'accord avec vos points et par exemple en Java, je ferais toujours respecter votre approche. Mais je pense que ROR est différent et voici pourquoi: Si vous voulez enregistrer ActiveRecord dans DB et que vous avez une méthode qui l'enregistre, vous devrez assembler le hachage avant de le transmettre pour enregistrer la méthode. Par exemple, nous définissons le premier, le nom, le nom d'utilisateur, etc. et ensuite passons le hachage pour enregistrer la méthode qui ferait quelque chose et l'enregistrerait. Et voici le problème: comment chaque développeur sait-il quoi mettre dans le hachage? Il s'agit d'un enregistrement actif, vous devrez donc aller voir le schéma de la base de données plutôt que d'assembler le hachage, et faire très attention à ne manquer aucun symbole.
Haris Krajina
2

Si vous souhaitez modifier la signature de la méthode, vous pouvez faire quelque chose comme ceci:

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

Dans ce cas, interpolé argsou opts.valuessera un tableau, mais vous pouvez joinsi sur virgule. À votre santé

Simon Bagreev
la source
2

Il semble que ce que cette question essaie d'accomplir pourrait être fait avec un joyau que je viens de publier, https://github.com/ericbeland/exception_details . Il listera les variables locales et les vlaues (et les variables d'instance) des exceptions sauvées. Ça vaut peut-être le coup d'œil ...

ebeland
la source
1
C'est un joli bijou, pour les utilisateurs de Rails, je recommanderais également le better_errorsbijou.
Haris Krajina
1

Si vous avez besoin d'arguments en tant que hachage et que vous ne voulez pas polluer le corps de la méthode avec une extraction délicate des paramètres, utilisez ceci:

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

Ajoutez simplement cette classe à votre projet:

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
dollar
la source
0

Si la fonction est à l'intérieur d'une classe, vous pouvez faire quelque chose comme ceci:

class Car
  def drive(speed)
  end
end

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

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