如何检查 ruby 方法中可选参数的默认值是多少?
How can I inspect what is the default value for optional parameter in ruby's method?
给定 class,
class MyClass
def index(arg1, arg2="hello")
end
end
是否可以通过 Class#instance_method
等方法获取 arg2
的默认值?
我认为这种实用程序不可用的原因是默认参数的值在必须分配时被评估。因此,尝试评估它们可能会产生副作用。
让我告诉你一个关于俄罗斯政府核计划的故事:
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...
这是一个穷人的尝试:
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"
显然很容易出错:
- 不适用于本机方法
- 不适用于 REPL 中定义的方法
- 您需要读取权限
- 正则表达式不能处理很多情况(比如更复杂的默认值有空格,
)
或 ,
),但这可以改进。
如果有人想出一个纯粹内省的解决方案,那就去吧。
似乎我们可以检查方法参数值的唯一方法是访问方法的 binding
。使用 Tracepoint
class,我们可以获得这样一个绑定对象,然后检查所有 optional
参数的值。
我们需要确保只使用必需的参数调用所需的方法,以便为默认参数分配默认值。
下面是我的尝试 - 它适用于实例方法和 class 方法。为了调用实例方法,我们需要实例化 class - 如果构造函数需要参数,那么创建对象可能会很棘手。为避免该问题,此代码动态创建给定 class 的 sub-class 并为其定义 no-arg 构造函数。
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"}
给定 class,
class MyClass
def index(arg1, arg2="hello")
end
end
是否可以通过 Class#instance_method
等方法获取 arg2
的默认值?
我认为这种实用程序不可用的原因是默认参数的值在必须分配时被评估。因此,尝试评估它们可能会产生副作用。
让我告诉你一个关于俄罗斯政府核计划的故事:
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 argumentsimulation_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 ofsimulation_number:
? What is it? Surely, the implementation has some check like__actually_launch_nukes__ if simulation_number.nil?
. But is it reallynil
? 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 runninglaunch_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...
这是一个穷人的尝试:
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"
显然很容易出错:
- 不适用于本机方法
- 不适用于 REPL 中定义的方法
- 您需要读取权限
- 正则表达式不能处理很多情况(比如更复杂的默认值有空格,
)
或,
),但这可以改进。
如果有人想出一个纯粹内省的解决方案,那就去吧。
似乎我们可以检查方法参数值的唯一方法是访问方法的 binding
。使用 Tracepoint
class,我们可以获得这样一个绑定对象,然后检查所有 optional
参数的值。
我们需要确保只使用必需的参数调用所需的方法,以便为默认参数分配默认值。
下面是我的尝试 - 它适用于实例方法和 class 方法。为了调用实例方法,我们需要实例化 class - 如果构造函数需要参数,那么创建对象可能会很棘手。为避免该问题,此代码动态创建给定 class 的 sub-class 并为其定义 no-arg 构造函数。
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"}