如何检查ruby方法中可选参数的默认值是什么?

时间:2022-10-30 08:09:08

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?

是否可以通过类#instance_method等方法获取arg2的默认值?

2 个解决方案

#1


4  

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.

前一段时间,他们聘请了超级核心的俄罗斯黑客,提出了一个既防错也又安全的大型解决方案,既可以启动所有可用的核武器,也可以简单地运行模拟。他们决定创建一个名为launch_all_nukes的方法,该方法可选择接受关键字参数simulation_number:。他们在REPL中加载了实现并删除了代码,因此敌人的间谍永远无法知道它是如何工作的。


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.

...即使这些平均需要25分钟,有时候感觉就像几个小时。

$: 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?

... Tik-tok,tik-tok,tik-tok ...想知道午餐会有什么?

$: 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!

......终于来了! 7总是最有趣的。它是唯一一个有时表明有0.03%-0.08%的机会不完全毁灭的机会。伊万不知道数字7背后是什么。或者其他任何模拟。他只是运行命令并等待。但可以肯定的是,7号是他在其他沉闷的任务中带来一丝欢乐和兴奋的人。 Aaaaaaand,去吧!

$: launch_all_nukes simulation_number: 7

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

...... 0%和所有其他人一样。多么规律。

$: 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? ...

...等一下...... simulation_number的默认值是什么?它是什么?当然,如果是simulation_number.nil,那么实现有一些像__actually_launch_nukes__的检查。但它真的没有?或者是别的什么? ...

$: 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没有任何参数提示三个不同的访问密钥,他都不知道。

$: 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...

... Ivan之前在控制台中运行过普通的Ruby命令。无论如何,它只是一个常规的问题...只是运行一个简单的内省方法......他知道他不被允许这样做......但没有人会知道,对吧?甚至没有人知道这个程序是如何工作的......啊......

$: 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!

... 13和14是最糟糕的! 13通常需要一个半小时。 14甚至更长。该死的,伊万渴望,只是一个微不足道的小信息,让他的思绪至少保持几分钟...让我们做!

$: 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
  • 不适用于REPL中定义的方法
  • 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.

如果有人想出一个纯粹内省的解决方案,那就去吧。

#2


4  

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.

似乎我们只能通过访问方法绑定来检查方法参数的值。使用Tracepoint类,我们可以获取这样的绑定对象,然后检查所有可选参数的值。

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"}

#1


4  

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.

前一段时间,他们聘请了超级核心的俄罗斯黑客,提出了一个既防错也又安全的大型解决方案,既可以启动所有可用的核武器,也可以简单地运行模拟。他们决定创建一个名为launch_all_nukes的方法,该方法可选择接受关键字参数simulation_number:。他们在REPL中加载了实现并删除了代码,因此敌人的间谍永远无法知道它是如何工作的。


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.

...即使这些平均需要25分钟,有时候感觉就像几个小时。

$: 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?

... Tik-tok,tik-tok,tik-tok ...想知道午餐会有什么?

$: 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!

......终于来了! 7总是最有趣的。它是唯一一个有时表明有0.03%-0.08%的机会不完全毁灭的机会。伊万不知道数字7背后是什么。或者其他任何模拟。他只是运行命令并等待。但可以肯定的是,7号是他在其他沉闷的任务中带来一丝欢乐和兴奋的人。 Aaaaaaand,去吧!

$: launch_all_nukes simulation_number: 7

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

...... 0%和所有其他人一样。多么规律。

$: 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? ...

...等一下...... simulation_number的默认值是什么?它是什么?当然,如果是simulation_number.nil,那么实现有一些像__actually_launch_nukes__的检查。但它真的没有?或者是别的什么? ...

$: 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没有任何参数提示三个不同的访问密钥,他都不知道。

$: 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...

... Ivan之前在控制台中运行过普通的Ruby命令。无论如何,它只是一个常规的问题...只是运行一个简单的内省方法......他知道他不被允许这样做......但没有人会知道,对吧?甚至没有人知道这个程序是如何工作的......啊......

$: 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!

... 13和14是最糟糕的! 13通常需要一个半小时。 14甚至更长。该死的,伊万渴望,只是一个微不足道的小信息,让他的思绪至少保持几分钟...让我们做!

$: 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
  • 不适用于REPL中定义的方法
  • 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.

如果有人想出一个纯粹内省的解决方案,那就去吧。

#2


4  

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.

似乎我们只能通过访问方法绑定来检查方法参数的值。使用Tracepoint类,我们可以获取这样的绑定对象,然后检查所有可选参数的值。

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"}