Ruby -关键字参数-你能把所有的关键字参数当作一个散列吗?怎么做?

nzkunb0c  于 2023-10-17  发布在  Ruby
关注(0)|答案(6)|浏览(169)

我有一个方法,看起来像这样:

def method(:name => nil, :color => nil, shoe_size => nil) 
  SomeOtherObject.some_other_method(THE HASH THAT THOSE KEYWORD ARGUMENTS WOULD MAKE)
end

对于任何给定的调用,我可以接受任意可选值的组合。我喜欢命名参数,因为我可以查看方法的签名来查看可用的选项。
我不知道的是,是否有一个快捷方式,我所描述的大写字母在上面的代码示例。
回到奥登时代,它曾经是:

def method(opts)
  SomeOtherObject.some_other_method(opts)
end

优雅,简单,几乎是欺骗。
对于那些关键字参数,是否有一个快捷方式,或者我必须在方法调用中重新构造我的选项哈希?

irlmq6kh

irlmq6kh1#

是的,这是可能的,但它不是很优雅。
您必须使用parameters方法,该方法返回方法参数及其类型的数组(在本例中,我们只有关键字参数)。

def foo(one: 1, two: 2, three: 3)
  method(__method__).parameters
end  
#=> [[:key, :one], [:key, :two], [:key, :three]]

知道了这一点,有各种方法可以使用该数组来获取所有参数及其提供的值的散列。

def foo(one: 1, two: 2, three: 3)
  params = method(__method__).parameters.map(&:last)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}

所以你的例子看起来像

def method(name: nil, color: nil, shoe_size: nil)
  opts = method(__method__).parameters.map(&:last).map { |p| [p, eval(p.to_s)] }.to_h
  SomeOtherObject.some_other_method(opts)
end

仔细考虑使用这个。它很聪明,但以可读性为代价,其他阅读您的代码的人不会喜欢它。
您可以使用帮助器方法使其更具可读性。

def params # Returns the parameters of the caller method.
  caller_method = caller_locations(length=1).first.label  
  method(caller_method).parameters 
end

def method(name: nil, color: nil, shoe_size: nil)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
  SomeOtherObject.some_other_method(opts)
end

**更新:**Ruby 2.2引入了Binding#local_variables,可以用来代替Method#parameters。要小心,因为在方法内部定义任何其他局部变量之前,必须调用local_variables

# Using Method#parameters
def foo(one: 1, two: 2, three: 3)
  params = method(__method__).parameters.map(&:last)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}

# Using Binding#local_variables (Ruby 2.2+)
def bar(one: 1, two: 2, three: 3)
  binding.local_variables.params.map { |p|
    [p, binding.local_variable_get(p)]
  }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}
o2gm4chl

o2gm4chl2#

当然!只要使用双splat(**)运算符。

def print_all(**keyword_arguments)
  puts keyword_arguments
end

def mixed_signature(some: 'option', **rest)
  puts some
  puts rest
end

print_all example: 'double splat (**)', arbitrary: 'keyword arguments'
# {:example=>"double splat (**)", :arbitrary=>"keyword arguments"}

mixed_signature another: 'option'
# option
# {:another=>"option"}

它的工作原理与常规splat(*)类似,用于收集参数。您甚至可以将关键字参数转发给另一个方法。

def forward_all(*arguments, **keyword_arguments, &block)
  SomeOtherObject.some_other_method *arguments,
                                    **keyword_arguments,
                                    &block
end
0ejtzxu1

0ejtzxu13#

下面的语法怎么样?
为了使它工作,将params作为方法中的保留关键字,并将这一行放在方法的顶部。

def method(:name => nil, :color => nil, shoe_size => nil) 
  params = params(binding)

  # params now contains the hash you're looking for
end

class Object
  def params(parent_binding)
    params = parent_binding.local_variables.reject { |s| s.to_s.start_with?('_') || s == :params }.map(&:to_sym)

    return params.map { |p| [ p, parent_binding.local_variable_get(p) ] }.to_h
  end
end
n8ghc7c1

n8ghc7c14#

我玩得很开心,所以谢谢你。我的想法是

describe "Argument Extraction Experiment" do
  let(:experiment_class) do
    Class.new do
      def method_with_mixed_args(one, two = 2, three:, four: 4)
        extract_args(binding)
      end

      def method_with_named_args(one:, two: 2, three: 3)
        extract_named_args(binding)
      end

      def method_with_unnamed_args(one, two = 2, three = 3)
        extract_unnamed_args(binding)
      end

      private

      def extract_args(env, depth = 1)
        caller_param_names = method(caller_locations(depth).first.label).parameters
        caller_param_names.map do |(arg_type,arg_name)|
          { name: arg_name, value: eval(arg_name.to_s, env), type: arg_type }
        end
      end

      def extract_named_args(env)
        extract_args(env, 2).select {|arg| [:key, :keyreq].include?(arg[:type]) }
      end

      def extract_unnamed_args(env)
        extract_args(env, 2).select {|arg| [:opt, :req].include?(arg[:type]) }
      end
    end
  end

  describe "#method_with_mixed_args" do
    subject { experiment_class.new.method_with_mixed_args("uno", three: 3) }
    it "should return a list of the args with values and types" do
      expect(subject).to eq([
        { name: :one,    value: "uno", type: :req },
        { name: :two,    value: 2,     type: :opt },
        { name: :three,  value: 3,     type: :keyreq },
        { name: :four,   value: 4,     type: :key }
      ])
    end
  end

  describe "#method_with_named_args" do
    subject { experiment_class.new.method_with_named_args(one: "one", two: 4) }
    it "should return a list of the args with values and types" do
      expect(subject).to eq([
        { name: :one,    value: "one", type: :keyreq },
        { name: :two,    value: 4,     type: :key },
        { name: :three,  value: 3,     type: :key }
      ])
    end
  end

  describe "#method_with_unnamed_args" do
    subject { experiment_class.new.method_with_unnamed_args(2, 4, 6) }
    it "should return a list of the args with values and types" do
      expect(subject).to eq([
        { name: :one,    value: 2,  type: :req },
        { name: :two,    value: 4,  type: :opt },
        { name: :three,  value: 6,  type: :opt }
      ])
    end
  end
end

我选择返回一个数组,但你可以很容易地修改它以返回一个哈希值(例如,在初始检测后不关心参数类型)。

vc6uscn9

vc6uscn95#

@丹尼斯的回答很有用,也很有教育意义。然而,我注意到Binding#local_variables将返回 * 所有 * 局部变量,无论local_variables何时执行:

def locals_from_binding(binding_:)
  binding_.local_variables.map { |var|
    [var, binding_.local_variable_get(var)]
  }.to_h
end

def m(a:, b:, c:)
  args = locals_from_binding(binding_: binding)
  pp args

  d = 4
end

m(a: 1, b: 3, c: 5)
# Prints:
#   {:a=>1, :b=>3, :c=>5, :args=>nil, :d=>nil}
# Note the presence of :d

我提出了一个混合解决方案:

def method_args_from_parameters(binding_:)
  method(caller_locations[0].label)
  .parameters.map(&:last)
  .map { |var|
    [var, binding_.local_variable_get(var)]
  }.to_h
end

def m(a:, b:, c:)
  args = method_args_from_parameters(binding_: binding)
  pp args

  d = 4
end

m(a: 1, b: 3, c: 5)
# Prints:
#   {:a=>1, :b=>3, :c=>5}
# Note the absence of :d
dffbzjpn

dffbzjpn6#

下面是一个例子,可以同时获取参数和关键字参数,而不管命名或***形式如何。

def print_args_and_kwargs(a, *arguments, foo:, **options)
  method(__method__).parameters.each_with_object({ args: [], kwargs: {} }) do |(type, name), memo|
    case type
    when :req, :opt
      memo[:args] << binding.local_variable_get(name)
    when :rest
      memo[:args].concat binding.local_variable_get(name)
    when :keyreq, :key
      memo[:kwargs][name] = binding.local_variable_get(name)
    when :keyrest
      memo[:kwargs].merge! binding.local_variable_get(name)
    end
  end.tap { pp _1 }
end
print_args_and_kwargs(1, 2, 3, foo: 1, bar: 2)

# {:args=>[1, 2, 3], :kwargs=>{:foo=>1, :bar=>2}}

通过mix-in使其可重用:

module Args
  private

  def args_and_kwargs(binding)
    method(caller_locations(1, 1).first.label).parameters.each_with_object([[], {}]) do |(type, name), memo|
      case type
      when :req, :opt
        memo.first << binding.local_variable_get(name)
      when :rest
        memo.first.concat binding.local_variable_get(name)
      when :keyreq, :key
        memo.last[name] = binding.local_variable_get(name)
      when :keyrest
        memo.last.merge! binding.local_variable_get(name)
      end
    end
  end
end

class A
  include Args

  def a(a, b = 2, *arguments, foo:, bar: 4, **options)
    args, kwargs = args_and_kwargs(binding)
    pp args, kwargs
  end
end
A.new.a(1, foo: 3)
# [1, 2]
# {:foo=>3, :bar=>4}

相关问题