【问题标题】:How do I extract a sub-hash from a hash?如何从哈希中提取子哈希?
【发布时间】:2012-02-19 23:15:21
【问题描述】:

我有一个哈希:

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}

【问题讨论】:

  • @JanDvorak 这个问题不仅是关于返回子哈希,而且是关于修改现有的。非常相似的事情,但 ActiveSupport 有不同的方法来处理它们。

标签: ruby hash


【解决方案1】:

如果你也想从数据库记录中提取,最好使用slice

hash = { a: 1, b: 2, c: 3, d: 4 }
hash.slice!(:a, :b)  # => {:c=>3, :d=>4}
hash                 # => {:a=>1, :b=>2}

https://api.rubyonrails.org/classes/Hash.html#method-i-slice-21

【讨论】:

    【解决方案2】:

    我们可以通过循环仅我们想要提取的键并检查该键是否存在然后提取它来做到这一点。

    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)
    

    【讨论】:

      【解决方案3】:

      只是对 slice 方法的一个补充,如果你想从原始哈希中分离出来的子哈希键是动态的,你可以这样做,

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

      【讨论】:

        【解决方案4】:

        正如其他人所提到的,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)
        

        【讨论】:

          【解决方案5】:

          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}
          

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

          【讨论】:

            【解决方案6】:

            如果您没有在 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"}]
            

            【讨论】:

              【解决方案7】:

              Ruby 2.5 添加了Hash#slice

              h = { a: 100, b: 200, c: 300 }
              h.slice(:a)           #=> {:a=>100}
              h.slice(:b, :c, :d)   #=> {:b=>200, :c=>300}
              

              【讨论】:

                【解决方案8】:

                这里是建议方法的快速性能比较,#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)
                

                【讨论】:

                  【解决方案9】:

                  如果你使用 railsHash#slice 是最好的选择。

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

                  如果你不使用 railsHash#values_at will return the values in the same order as you asked them,你可以这样做:

                  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
                  

                  【讨论】:

                  • Mokey 补丁绝对是 IMO 的必经之路。更清晰,使意图更清晰。
                  • 添加修改代码以正确处理核心模块,定义模块并导入扩展哈希核心...模块CoreExtensions模块哈希def slice(*keys) ::Hash[[keys, self.values_at(* keys)].transpose] end end end Hash.include CoreExtensions::Hash
                  【解决方案10】:

                  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 类。

                  【讨论】:

                  • x.except!(:c, :d)(带 bang)的结果应该是# => {:a=>1, :b=>2}。如果您可以编辑答案,那就太好了。
                  【解决方案11】:

                  您可以使用 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}
                  

                  extracted_slide 现在是

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

                  你可以看slice.rb in ActiveSupport 3.1.3

                  【讨论】:

                  • 我认为你在描述提取物!。提炼!从初始哈希中删除键,返回包含已删除键的新哈希。片!做相反的事情:从初始散列中删除所有 but 指定的键(同样,返回包含已删除键的新散列)。所以切片!有点像“保留”操作。
                  • ActiveSupport 不是 Ruby STI 的一部分
                  【解决方案12】:
                  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}
                  

                  【讨论】:

                    【解决方案13】:

                    如果你使用rails,使用Hash.except可能会很方便

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

                    【讨论】:

                      【解决方案14】:
                      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)
                      

                      【讨论】:

                        【解决方案15】:
                        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}
                        

                        【讨论】:

                        • 干得好。不完全是他要求的。您的方法返回: {:d=>:D, :b=>:B, :e=>nil, :f=>nil} {:c=>:C, :a=>:A, :d=> :D, :b=>:B}
                        • 等效的单行(也许更快)解决方案:
                           def subhash(*keys)      select {|k,v| keys.include?(k)}   end
                        【解决方案16】:

                        此代码将您要求的功能注入到 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}
                        

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

                        【讨论】:

                          【解决方案17】:

                          如果您特别希望方法返回提取的元素但 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} 
                          

                          【讨论】:

                          • 这是 O(n2) - 您将在 select 上有一个循环,在 include 上有另一个循环,将被称为 h1.size 次。
                          • 虽然这个答案对于纯红宝石来说是不错的,但如果你使用的是 rails,下面的答案(使用内置的 sliceexcept,取决于你的需要)要干净得多跨度>
                          • .slice & .except 是正确答案,见下文
                          猜你喜欢
                          • 2010-12-19
                          • 2013-01-23
                          • 1970-01-01
                          • 1970-01-01
                          • 2020-07-21
                          • 1970-01-01
                          • 1970-01-01
                          • 2011-04-20
                          • 2011-06-05
                          相关资源
                          最近更新 更多