How can I inspect what is the default value for op

2020-06-16 01:53发布

Given a class,

class MyClass
  def index(arg1, arg2="hello")
  end
end

Is it possible to obtain the default value for arg2, via some methods like Class#instance_method, or something?

2条回答
戒情不戒烟
2楼-- · 2020-06-16 02:33

It seems that only way we can inspect the values of method arguments is by having access to binding of method. Using Tracepoint class, we can get hold of such a binding object and then inspect the values of all optional parameters.

We need to ensure that we invoke the desired method with only required parameters, so that default parameters gets assigned their default values.

Below is my attempt to do this - it works with both instance methods and class methods. In order to invoke instance methods, we need to instantiate the class - if the constructor requires parameters, then, it can get tricky to create an object. To circumvent that issue, this code dynamically creates a sub-class of given class and defines a no-arg constructor for it.

class MyClass

  # one arg constructor to make life complex
  def initialize param
  end

  def index(arg1, arg2="hello", arg3 = 1, arg4 = {a:1}, arg5 = [1,2,3])
    raise "Hi"  # for testing purpose
  end

  def self.hi(arg6, arg7="default param")
  end
end

def opt_values(clazz, meth)
    captured_binding = nil

    TracePoint.new(:call) do |tp|
        captured_binding = tp.binding
    end.enable {
        # Dummy sub-class so that we can create instances with no-arg constructor
        obj = Class.new(clazz) do
            def initialize
            end
        end.new

        # Check if it's a class method
        meth_obj = clazz.method(meth) rescue nil

        # If not, may be an instance method.
        meth_obj = obj.method(meth) rescue nil if not meth_obj

        if meth_obj
            params = meth_obj.parameters
            optional_params = params.collect {|i| i.last if i.first == :opt}.compact
            dummy_required_params = [""] * (params.size - optional_params.size)

            # Invoke the method, and handle any errors raise            
            meth_obj.call *dummy_required_params rescue nil

            # Create a hash for storing optional argument name and its default value
            optional_params.each_with_object({}) do |i, hash|
                hash[i] = captured_binding.local_variable_get(i)
            end
        end
    }
end

p opt_values MyClass, :index
#=> {:arg2=>"hello", :arg3=>1, :arg4=>{:a=>1}, :arg5=>[1, 2, 3]}
p opt_values MyClass, :hi
#=> {:arg7=>"default param"}
查看更多
家丑人穷心不美
3楼-- · 2020-06-16 02:38

I think the reason such utility is not made available is that the values of the default arguments are evaluated when they have to be assigned. Therefore, trying to evaluate them might have side additional effects.


Let me tell you a story about the Russian government's nuclear plans:

Some time ago, they hired ultra hardcore Russian hackers to come up with a a solution that is both error-proof and mega secure that allows to either launch all available nukes or simply run a simulation. They decided to create one method called launch_all_nukes, which optionally accepts a keyword argument simulation_number:. They loaded the implementation in a REPL and deleted the code so enemy spies could never find out how it actually works.


Each day for the past couple of years, the trusted specialist Ivan travels to a giga secret location where he sits in front of what looks to be a regular irb and evaluates the chances of the Russian Federation surviving a supposed mutual assured destruction.

$: launch_all_nukes simulation_number: 1

...
Just another regular day.

$: launch_all_nukes simulation_number: 2

...

$: launch_all_nukes simulation_number: 3

...
Even though these take 25 minutes on average, it feels like hours sometimes.

$: launch_all_nukes simulation_number: 4

...
Staring at the screen. Just another regular day. Another... regular... day...

$: launch_all_nukes simulation_number: 5

...
Tik-tok, tik-tok, tik-tok... Wondering what might there be for lunch?

$: launch_all_nukes simulation_number: 6

...
Finally! 7 is always the most interesting. It's the only one that sometimes shows there is a 0.03% - 0.08% chance of not complete annihilation. Ivan has no idea what stands behind the number 7. Or any of the other simulations for that matter. He just runs commands and waits. But surely, number 7 is the one that brings little beams of joy and excitement in his otherwise dull assignment. Aaaaaaand, go!

$: launch_all_nukes simulation_number: 7

...
0%. As all the others. How regular.

$: launch_all_nukes simulation_number: 8

...
Does it matter, actually? Why would one nation be superior to all the others? Is human life valuable by itself to begin with? Is Earth as a whole inherently valuable? Just a tiny spectacle of rock floating in an endless Universe...

$: launch_all_nukes simulation_number: 9

...
What happened? Ivan used to be a great developer. And now he just stares at a console, running repetitive commands from time to time... Is this what progress feels like...

$: launch_all_nukes simulation_number: 10

...
Wait a second... What is the default value of simulation_number:? What is it? Surely, the implementation has some check like __actually_launch_nukes__ if simulation_number.nil?. But is it really nil? Or is it something else? ...

$: launch_all_nukes simulation_number: 11

...
Like a repetitive earworm, this tiny question never left his mind... what is it? ... He never feared accidentally endangering the world because he saw that running launch_all_nukes with no arguments prompts for three different access keys, none of which he knows.

$: launch_all_nukes simulation_number: 12

...
Ivan has ran ordinary Ruby commands in the console before. By all means, it's just a regular irb... Just running one simple introspection method... He knows he is not allowed to do it... But no one will know, right? No one even knows how this program works anyway... Ah...

$: launch_all_nukes simulation_number: 13

...
13 and 14 are the worst! 13 usually takes an hour and a half. 14 is even longer. Damn it, Ivan craves, just an itsy bitsy tiny information to keep his mind engaged for at least a couple of minutes... Lets do it!

$: method(:launch_all_nukes).default_value_for(:simulation_number)

...
Mortified, Ivan froze motionless as the sudden realization hit him. He now knows what the default value is. But it is too late...


Here is a poor man's attempt:

argument_name = 'arg2'

origin_file, definition_line = MyClass.instance_method(:index).source_location
method_signature = IO.readlines(origin_file)[definition_line.pred]
eval(method_signature.match(/#{argument_name}\s*[=:]\s*\K[^\s),]*/)[0]) # => "hello"

Obviously very error prone:

  • Doesn't work with native methods
  • Doesn't work with methods defined in REPLs
  • You need read privileges
  • The regex doesn't handle many cases (like more complex default values that have whitespaces, ) or , in them), but this can be improved.

If someone comes up with a purely introspective solution, go with that.

查看更多
登录 后发表回答