如何从哈希中提取子哈希?


95

我有一个哈希:

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}

提取像这样的子哈希的最佳方法是什么?

h1.extract_subhash(:b, :d, :e, :f) # => {:b => :B, :d => :D}
h1 #=> {:a => :A, :c => :C}



1
@JanDvorak这个问题不仅与返回subhash有关,而且与修改现有的有关。非常相似的事情,但是ActiveSupport拥有不同的处理方式。
skalee 2013年

Answers:


58

如果您特别希望该方法返回提取的元素,但h1保持不变:

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.select {|key, value| [:b, :d, :e, :f].include?(key) } # => {:b=>:B, :d=>:D} 
h1 = Hash[h1.to_a - h2.to_a] # => {:a=>:A, :c=>:C} 

如果要将其修补到Hash类中:

class Hash
  def extract_subhash(*extract)
    h2 = self.select{|key, value| extract.include?(key) }
    self.delete_if {|key, value| extract.include?(key) }
    h2
  end
end

如果只想从哈希中删除指定的元素,则使用delete_if会容易得多。

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h1.delete_if {|key, value| [:b, :d, :e, :f].include?(key) } # => {:a=>:A, :c=>:C} 
h1  # => {:a=>:A, :c=>:C} 

2
这是O(n2)-您将在select上有一个循环,在include上有另一个循环,这将称为h1.size times。
metakungfu 2015年

1
尽管此答案对于纯红宝石来说是不错的选择,但如果您使用滑轨,则以下答案(使用内置sliceexcept,具体取决于您的需求)更加干净
Krease

137

ActiveSupport在自2.3.8至少,提供了四个方便的方法:#slice#except和其破坏性同行:#slice!#except!。在其他答案中提到了它们,但将它们汇总在一个地方:

x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.slice(:a, :b)
# => {:a=>1, :b=>2}

x
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.except(:a, :b)
# => {:c=>3, :d=>4}

x
# => {:a=>1, :b=>2, :c=>3, :d=>4}

注意bang方法的返回值。他们不仅会定制现有的哈希,还会返回已删除(未保留)的条目。最Hash#except!适合问题中给出的示例:

x = {a: 1, b: 2, c: 3, d: 4}
# => {:a=>1, :b=>2, :c=>3, :d=>4}

x.except!(:c, :d)
# => {:a=>1, :b=>2}

x
# => {:a=>1, :b=>2}

ActiveSupport不需要整个Rails,相当轻巧。实际上,很多非Rails gem都依赖于它,因此很可能您已经在Gemfile.lock中拥有了它。无需自己扩展Hash类。


3
x.except!(:c, :d)(带有爆炸声)的结果应为# => {:a=>1, :b=>2}。好的,如果您可以编辑答案。
244

28

如果使用rails,则必须使用Hash#slice

{:a => :A, :b => :B, :c => :C, :d => :D}.slice(:a, :c)
# =>  {:a => :A, :c => :C}

如果不使用rails,则Hash#values_at将按照您要求的顺序返回值,因此您可以执行以下操作:

def slice(hash, *keys)
  Hash[ [keys, hash.values_at(*keys)].transpose]
end

def except(hash, *keys)
  desired_keys = hash.keys - keys
  Hash[ [desired_keys, hash.values_at(*desired_keys)].transpose]
end

例如:

slice({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2) 
# => {'bar' => 'foo', 2 => 'two'}

except({foo: 'bar', 'bar' => 'foo', 2 => 'two'}, 'bar', 2) 
# => {:foo => 'bar'}

说明:

出于{:a => 1, :b => 2, :c => 3}我们想{:a => 1, :b => 2}

hash = {:a => 1, :b => 2, :c => 3}
keys = [:a, :b]
values = hash.values_at(*keys) #=> [1, 2]
transposed_matrix =[keys, values].transpose #=> [[:a, 1], [:b, 2]]
Hash[transposed_matrix] #=> {:a => 1, :b => 2}

如果您觉得必须要修补猴子,请按照以下步骤进行:

module MyExtension
  module Hash 
    def slice(*keys)
      ::Hash[[keys, self.values_at(*keys)].transpose]
    end
    def except(*keys)
      desired_keys = self.keys - keys
      ::Hash[[desired_keys, self.values_at(*desired_keys)].transpose]
    end
  end
end
Hash.include MyExtension::Hash

2
Mokey修补绝对是IMO的方法。更清洁,意图更清晰。
罗马里奥

1
添加修改代码以寻址corectly核心模块,定义模块并导入扩展Hash核心... module CoreExtensions模块Hash def slice(* keys):: Hash [[keys,self.values_at(* keys)]。transpose]结束结束Hash.include CoreExtensions :: Hash
Ronan Fauglas


5

您可以使用ActiveSupport核心扩展中可用的slice!(* keys)

initial_hash = {:a => 1, :b => 2, :c => 3, :d => 4}

extracted_slice = initial_hash.slice!(:a, :c)

initial_hash现在应该是

{:b => 2, :d =>4}

extract_slide现在将是

{:a => 1, :c =>3}

你可以看一下 slice.rb in ActiveSupport 3.1.3


我认为您正在描述摘录!提取!从初始哈希中删除键,并返回一个包含已删除键的新哈希。片!不相反:删除所有从初始散列指定键(再次,返回包含去除的密钥的新的哈希)。切!有点像“保留”操作。
Russ Egan 2012年

1
ActiveSupport不属于Ruby STI
Volte

4
module HashExtensions
  def subhash(*keys)
    keys = keys.select { |k| key?(k) }
    Hash[keys.zip(values_at(*keys))]
  end
end

Hash.send(:include, HashExtensions)

{:a => :A, :b => :B, :c => :C, :d => :D}.subhash(:a) # => {:a => :A}

1
不错的工作。不完全是他的要求。您的方法返回:{:d =>:D,:b =>:B,:e => nil,:f => nil} {:c =>:C,:a =>:A,:d => :D,:b =>:B}
Andy

等效的单行(可能更快)的解决方案:<pre> def subhash(*keys) select {|k,v| keys.include?(k)} end
达到峰值

3
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
keys = [:b, :d, :e, :f]

h2 = (h1.keys & keys).each_with_object({}) { |k,h| h.update(k=>h1.delete(k)) }
  #=> {:b => :B, :d => :D}
h1
  #=> {:a => :A, :c => :C}

2

如果使用rails,使用Hash可能会很方便。

h = {a:1, b:2}
h1 = h.except(:a) # {b:2}

1
class Hash
  def extract(*keys)
    key_index = Hash[keys.map{ |k| [k, true] }] # depends on the size of keys
    partition{ |k, v| key_index.has_key?(k) }.map{ |group| Hash[group] }  
  end
end

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2, h1 = h1.extract(:b, :d, :e, :f)

1

这是建议方法的快速性能比较,#select似乎是最快的

k = 1_000_000
Benchmark.bmbm do |x|
  x.report('select') { k.times { {a: 1, b: 2, c: 3}.select { |k, _v| [:a, :b].include?(k) } } }
  x.report('hash transpose') { k.times { Hash[ [[:a, :b], {a: 1, b: 2, c: 3}.fetch_values(:a, :b)].transpose ] } }
  x.report('slice') { k.times { {a: 1, b: 2, c: 3}.slice(:a, :b) } }
end

Rehearsal --------------------------------------------------
select           1.640000   0.010000   1.650000 (  1.651426)
hash transpose   1.720000   0.010000   1.730000 (  1.729950)
slice            1.740000   0.010000   1.750000 (  1.748204)
----------------------------------------- total: 5.130000sec

                     user     system      total        real
select           1.670000   0.010000   1.680000 (  1.683415)
hash transpose   1.680000   0.010000   1.690000 (  1.688110)
slice            1.800000   0.010000   1.810000 (  1.816215)

细化如下所示:

module CoreExtensions
  module Extractable
    refine Hash do
      def extract(*keys)
        select { |k, _v| keys.include?(k) }
      end
    end
  end
end

并使用它:

using ::CoreExtensions::Extractable
{ a: 1, b: 2, c: 3 }.extract(:a, :b)

1

这两个delete_ifkeep_if是Ruby的核心的一部分。在这里,您无需修补Hash类型即可实现所需的功能。

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.clone
p h1.keep_if { |key| [:b, :d, :e, :f].include?(key) } # => {:b => :B, :d => :D}
p h2.delete_if { |key, value| [:b, :d, :e, :f].include?(key) } #=> {:a => :A, :c => :C}

有关更多信息,请查看文档中的以下链接:


1

正如其他人提到的,Ruby 2.5添加了Hash#slice方法。

Rails 5.2.0beta1还添加了它自己的Hash#slice版本,以填充使用早期版本Ruby的框架用户的功能。 https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8

如果出于某种原因想要实现自己的实现,那么它也是一个不错的选择:

 def slice(*keys)
   keys.each_with_object(Hash.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
 end unless method_defined?(:slice)

0

这段代码将您要求的功能注入到Hash类中:

class Hash
    def extract_subhash! *keys
      to_keep = self.keys.to_a - keys
      to_delete = Hash[self.select{|k,v| !to_keep.include? k}]
      self.delete_if {|k,v| !to_keep.include? k}
      to_delete
    end
end

并产生您提供的结果:

h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
p h1.extract_subhash!(:b, :d, :e, :f) # => {b => :B, :d => :D}
p h1 #=> {:a => :A, :c => :C}

注意:此方法实际上返回提取的键/值。


0

如果您不是在Ruby 2.5上运行,并且在不需要通过添加新方法来污染Hash类的情况下,以下是一个实用的解决方案:

slice_hash = -> keys, hash { hash.select { |k, _v| keys.include?(k) } }.curry

然后,您甚至可以将其应用于嵌套哈希:

my_hash = [{name: "Joe", age: 34}, {name: "Amy", age: 55}]
my_hash.map(&slice_hash.([:name]))
# => [{:name=>"Joe"}, {:name=>"Amy"}]

0

只是slice方法的一个补充,如果您想与原始哈希分开的subhash键是动态的,您可以这样做,

slice(*dynamic_keys) # dynamic_keys should be an array type 

0

我们可以通过仅在我们要提取的键上循环并仅检查键是否存在然后提取它来完成此操作。

class Hash
  def extract(*keys)
    extracted_hash = {}
    keys.each{|key| extracted_hash[key] = self.delete(key) if self.has_key?(key)}
    extracted_hash
  end
end
h1 = {:a => :A, :b => :B, :c => :C, :d => :D}
h2 = h1.extract(:b, :d, :e, :f)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.