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

Ram*_*oka 9 ruby metaprogramming introspection

鉴于一堂课,

class MyClass
  def index(arg1, arg2="hello")
  end
end
Run Code Online (Sandbox Code Playgroud)

是否可以arg2通过某些方法获取默认值Class#instance_method

ndn*_*kov 5

我认为此类实用程序不可用的原因是默认参数的值在必须分配时进行评估。因此,尝试评估它们可能会产生副作用。


让我给你讲一个关于俄罗斯政府核计划的故事:

不久前,他们聘请了超级核心的俄罗斯黑客,想出了一个既防错又超级安全的解决方案,允许发射所有可用的核武器或简单地运行模拟。他们决定创建一个名为 的方法launch_all_nukes,该方法可以选择接受关键字参数simulation_number:。他们将实现加载到REPL中并删除代码,这样敌方间谍就永远无法发现它的实际工作原理。


在过去的几年里,值得信赖的专家伊万每天都会前往一个巨大的秘密地点,在那里他坐在一个看似普通的 IRB 前面,评估俄罗斯联邦在所谓的相互保证毁灭中幸存下来的机会。

$: launch_all_nukes simulation_number: 1
Run Code Online (Sandbox Code Playgroud)

……
又是平常的一天。

$: launch_all_nukes simulation_number: 2
Run Code Online (Sandbox Code Playgroud)

...

$: launch_all_nukes simulation_number: 3
Run Code Online (Sandbox Code Playgroud)

...
尽管这些平均需要 25 分钟,但有时感觉就像几个小时。

$: launch_all_nukes simulation_number: 4
Run Code Online (Sandbox Code Playgroud)

……
盯着屏幕。又是一个平凡的一天。又是……正常的……一天……

$: launch_all_nukes simulation_number: 5
Run Code Online (Sandbox Code Playgroud)

...
抖音、抖音、抖音... 想知道午餐有什么吗?

$: launch_all_nukes simulation_number: 6
Run Code Online (Sandbox Code Playgroud)

...
最后!7总是最有趣的。这是唯一一个有时显示有 0.03% - 0.08% 的可能性不完全毁灭的概率。伊万不知道数字 7 背后是什么。或者任何其他与此相关的模拟。他只是运行命令并等待。但可以肯定的是,7 号是给他原本沉闷的任务带来一丝快乐和兴奋的人。啊啊啊啊,走吧!

$: launch_all_nukes simulation_number: 7
Run Code Online (Sandbox Code Playgroud)

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

$: launch_all_nukes simulation_number: 8
Run Code Online (Sandbox Code Playgroud)

...
这真的很重要吗?为什么一个国家会优于所有其他国家?人的生命本身就有价值吗?地球作为一个整体是否具有内在价值?只是漂浮在无尽宇宙中的一块微小岩石奇观......

$: launch_all_nukes simulation_number: 9
Run Code Online (Sandbox Code Playgroud)

...
发生了什么?伊万曾经是一位伟大的开发人员。而现在他只是盯着控制台,时不时的运行着重复的命令……这就是进步的感觉吗……

$: launch_all_nukes simulation_number: 10
Run Code Online (Sandbox Code Playgroud)

...
等一下... 的默认值是多少simulation_number:?它是什么?当然,实现有一些检查,例如__actually_launch_nukes__ if simulation_number.nil?. 但真的是这样吗nil?或者是别的什么?...

$: launch_all_nukes simulation_number: 11
Run Code Online (Sandbox Code Playgroud)

……
就像一只重复的耳虫一样,这个小小的问题从未离开过他的脑海……那是什么?...他从不担心意外地危及世界,因为他看到launch_all_nukes无参数运行会提示输入三个不同的访问密钥,而他都不知道其中任何一个。

$: launch_all_nukes simulation_number: 12
Run Code Online (Sandbox Code Playgroud)

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

$: launch_all_nukes simulation_number: 13
Run Code Online (Sandbox Code Playgroud)

...
13和14是最差的!13通常需要一个半小时。14甚至更长。该死的,伊万渴望一个小小的信息,让他的思绪至少保持几分钟……让我们开始吧!

$: method(:launch_all_nukes).default_value_for(:simulation_number)
Run Code Online (Sandbox Code Playgroud)

……
伊万突然意识到这一点,他一动不动地僵住了。他现在知道默认值是什么。但为时已晚...


这是一个穷人的尝试:

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"
Run Code Online (Sandbox Code Playgroud)

显然很容易出错:

  • 不适用于本机方法
  • 不适用于 REPL 中定义的方法
  • 您需要阅读权限
  • 正则表达式不能处理很多情况(例如具有空格),其中包含空格的更复杂的默认值),但这可以改进。

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


Wan*_*ker 5

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

我们需要确保只使用必需的参数调用所需的方法,以便为默认参数分配其默认值.

下面是我尝试这样做 - 它适用于实例方法和类方法.为了调用实例方法,我们需要实例化类 - 如果构造函数需要参数,那么创建对象会变得棘手.为了避免这个问题,这段代码动态地创建了一个给定类的子类,并为它定义了一个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"}
Run Code Online (Sandbox Code Playgroud)