Accediendo a elementos de hash nesteds en ruby ​​

Estoy trabajando en una pequeña utilidad escrita en ruby ​​que hace un uso extensivo de hashes nesteds. Actualmente, estoy verificando el acceso a los elementos hash nesteds de la siguiente manera:

structure = { :a => { :b => 'foo' }} # I want structure[:a][:b] value = nil if structure.has_key?(:a) && structure[:a].has_key?(:b) then value = structure[:a][:b] end 

¿Hay una mejor manera de hacer esto? Me gustaría poder decir:

 value = structure[:a][:b] 

Y obtenga nil si: a no es una clave en la structure , etc.

Tradicionalmente, realmente tenías que hacer algo como esto:

 structure[:a] && structure[:a][:b] 

Sin embargo, Ruby 2.3 agregó una característica que hace de esta manera más elegante:

 structure.dig :a, :b # nil if it misses anywhere along the way 

Hay una gem llamada ruby_dig que hará un parche de respaldo para usted.

Ruby 2.3.0 introdujo un nuevo método llamado dig en ambos Hash y Array que resuelve este problema por completo.

 value = structure.dig(:a, :b) 

Devuelve nil si la clave falta en cualquier nivel.

Si está usando una versión de Ruby anterior a 2.3, puede usar la gem ruby_dig o implementarla usted mismo:

 module RubyDig def dig(key, *rest) if value = (self[key] rescue nil) if rest.empty? value elsif value.respond_to?(:dig) value.dig(*rest) end end end end if RUBY_VERSION < '2.3' Array.send(:include, RubyDig) Hash.send(:include, RubyDig) end 

La forma en que generalmente hago esto estos días es:

 h = Hash.new { |h,k| h[k] = {} } 

Esto le dará un hash que crea un hash nuevo como la entrada de una clave faltante, pero devuelve nil para el segundo nivel de clave:

 h['foo'] -> {} h['foo']['bar'] -> nil 

Puede anidar esto para agregar varias capas que se pueden abordar de esta manera:

 h = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = {} } } h['bar'] -> {} h['tar']['zar'] -> {} h['scar']['far']['mar'] -> nil 

También puede encadenar indefinidamente utilizando el método default_proc :

 h = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) } h['bar'] -> {} h['tar']['star']['par'] -> {} 

El código anterior crea un hash cuyo proc por defecto crea un nuevo Hash con el mismo proceso predeterminado. Por lo tanto, un hash creado como valor predeterminado cuando se produce una búsqueda de una clave no vista tendrá el mismo comportamiento predeterminado.

EDITAR: Más detalles

Los hashes de Ruby le permiten controlar cómo se crean los valores predeterminados cuando se produce una búsqueda para una nueva clave. Cuando se especifica, este comportamiento se encapsula como un objeto Proc y se puede acceder a través de los métodos default_proc y default_proc= . El proceso predeterminado también puede especificarse pasando un bloque a Hash.new .

Vamos a romper este código un poco. Esto no es un Ruby idiomático, pero es más fácil dividirlo en múltiples líneas:

 1. recursive_hash = Hash.new do |h, k| 2. h[k] = Hash.new(&h.default_proc) 3. end 

La línea 1 declara una variable recursive_hash como un nuevo Hash y comienza un bloque como el valor recursive_hash de recursive_hash . Al bloque se le pasan dos objetos: h , que es la instancia Hash la que se realiza la búsqueda de claves, k , la clave que se está buscando.

La línea 2 establece el valor predeterminado en el hash a una nueva instancia de Hash . El comportamiento predeterminado para este hash se proporciona al pasar un Proc creado a partir del default_proc del hash en el que se está realizando la búsqueda; es decir, el proceso predeterminado que define el bloque.

Aquí hay un ejemplo de una sesión de IRB:

 irb(main):011:0> recursive_hash = Hash.new do |h,k| irb(main):012:1* h[k] = Hash.new(&h.default_proc) irb(main):013:1> end => {} irb(main):014:0> recursive_hash[:foo] => {} irb(main):015:0> recursive_hash => {:foo=>{}} 

Cuando se creó el hash en recursive_hash[:foo] , el default_proc fue proporcionado por recursive_hash ‘s default_proc . Esto tiene dos efectos:

  1. El comportamiento predeterminado para recursive_hash[:foo] es el mismo que recursive_hash .
  2. El comportamiento predeterminado para los hashes creados por el pro ceso_de_recursos_recostivo recursive_hash[:foo] será el mismo que recursive_hash .

Entonces, continuando en IRB, obtenemos lo siguiente:

 irb(main):016:0> recursive_hash[:foo][:bar] => {} irb(main):017:0> recursive_hash => {:foo=>{:bar=>{}}} irb(main):018:0> recursive_hash[:foo][:bar][:zap] => {} irb(main):019:0> recursive_hash => {:foo=>{:bar=>{:zap=>{}}}} 

Hice Rubygem para esto. Prueba la vid

Instalar:

 gem install vine 

Uso:

 hash.access("abc") 

Creo que una de las soluciones más legibles es usar Hashie :

 require 'hashie' myhash = Hashie::Mash.new({foo: {bar: "blah" }}) myhash.foo.bar => "blah" myhash.foo? => true # use "underscore dot" for multi-level testing myhash.foo_.bar? => true myhash.foo_.huh_.what? => false 
 value = structure[:a][:b] rescue nil 

Solución 1

Sugerí esto en mi pregunta antes:

 class NilClass; def to_hash; {} end end 

Hash#to_hash ya está definido, y regresa a self. Entonces puedes hacer:

 value = structure[:a].to_hash[:b] 

to_hash garantiza que obtenga un hash vacío cuando la búsqueda de clave anterior falla.

Solution2

Esta solución es similar en espíritu a mu, es una respuesta demasiado corta ya que usa una subclase, pero aún algo diferente. En caso de que no haya valor para una determinada clave, no utiliza un valor predeterminado, sino que crea un valor de hash vacío, de modo que no tiene el problema de confusión en la asignación que la respuesta de DigitalRoss tiene, como se señaló por mu es demasiado corto.

 class NilFreeHash < Hash def [] key; key?(key) ? super(key) : self[key] = NilFreeHash.new end end structure = NilFreeHash.new structure[:a][:b] = 3 p strucrture[:a][:b] # => 3 

Sin embargo, se aparta de la especificación dada en la pregunta. Cuando se da una clave indefinida, devolverá un instinto de hash vacío de nil .

 p structure[:c] # => {} 

Si construye una instancia de este NilFreeHash desde el principio y asigna los pares clave-valor, funcionará, pero si desea convertir un hash en una instancia de esta clase, puede ser un problema.

Podrías simplemente construir una subclase hash con un método variadico adicional para cavar todo el camino hacia abajo con los controles apropiados a lo largo del camino. Algo como esto (con un nombre mejor por supuesto):

 class Thing < Hash def find(*path) path.inject(self) { |h, x| return nil if(!h.is_a?(Thing) || h[x].nil?); h[x] } end end 

Entonces solo usa Thing s en lugar de hash:

 >> x = Thing.new => {} >> x[:a] = Thing.new => {} >> x[:a][:b] = 'k' => "k" >> x.find(:a) => {:b=>"k"} >> x.find(:a, :b) => "k" >> x.find(:a, :b, :c) => nil >> x.find(:a, :c, :d) => nil 
 require 'xkeys' structure = {}.extend XKeys::Hash structure[:a, :b] # nil structure[:a, :b, :else => 0] # 0 (contextual default) structure[:a] # nil, even after above structure[:a, :b] = 'foo' structure[:a, :b] # foo 

Esta función de parche de mono para Hash debería ser la más fácil (al menos para mí). Tampoco altera la estructura, es decir, cambiando nil a {} . También se aplicaría incluso si está leyendo un árbol desde una fuente en bruto, por ejemplo, JSON. Tampoco necesita producir objetos hash vacíos a medida que avanza o analizar una cadena. rescue nil fue en realidad una buena solución para mí, ya que soy lo suficientemente valiente para un riesgo tan bajo, pero creo que esencialmente tiene un inconveniente con el rendimiento.

 class ::Hash def recurse(*keys) v = self[keys.shift] while keys.length > 0 return nil if not v.is_a? Hash v = v[keys.shift] end v end end 

Ejemplo:

 > structure = { :a => { :b => 'foo' }} => {:a=>{:b=>"foo"}} > structure.recurse(:a, :b) => "foo" > structure.recurse(:a, :x) => nil 

Lo que también es bueno es que puedes jugar con arreglos guardados con él:

 > keys = [:a, :b] => [:a, :b] > structure.recurse(*keys) => "foo" > structure.recurse(*keys, :x1, :x2) => nil 

Puedes usar la joya andand y la gem, pero me vuelvo cada vez más cauteloso:

 >> structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}} >> require 'andand' #=> true >> structure[:a].andand[:b] #=> "foo" >> structure[:c].andand[:b] #=> nil 

Existe la linda pero incorrecta forma de hacer esto. Lo cual es un parche de monos NilClass para agregar un método [] que devuelve nil . Digo que es el enfoque equivocado porque no tienes idea de qué otro software puede haber hecho una versión diferente, o qué cambio de comportamiento en una versión futura de Ruby se puede romper con esto.

Un mejor enfoque es crear un nuevo objeto que funcione como nil pero que sea compatible con este comportamiento. Haga de este nuevo objeto el retorno predeterminado de sus hashes. Y luego solo funcionará.

Alternativamente, puede crear una función simple de “búsqueda anidada” a la que le pase el hash y las teclas, que atraviesa los hash en orden y se activa cuando puede.

Yo personalmente preferiría uno de los dos últimos enfoques. Aunque creo que sería lindo si el primero estuviera integrado en el lenguaje Ruby. (Pero el parche de monos es una mala idea. No hagas eso, en particular para no demostrar que eres un hacker genial).

No es que lo haga, pero puedes usar Monkeypatch en NilClass#[] :

 > structure = { :a => { :b => 'foo' }} #=> {:a=>{:b=>"foo"}} > structure[:x][:y] NoMethodError: undefined method `[]' for nil:NilClass from (irb):2 from C:/Ruby/bin/irb:12:in `
' > class NilClass; def [](*a); end; end #=> nil > structure[:x][:y] #=> nil > structure[:a][:y] #=> nil > structure[:a][:b] #=> "foo"

Ve con la respuesta de @DigitalRoss. Sí, es más tipeo, pero es porque es más seguro.

En mi caso, necesitaba una matriz bidimensional donde cada celda es una lista de elementos.

Encontré esta técnica que parece funcionar. Podría funcionar para el OP:

 $all = Hash.new() def $all.[](k) v = fetch(k, nil) return v if v h = Hash.new() def h.[](k2) v = fetch(k2, nil) return v if v list = Array.new() store(k2, list) return list end store(k, h) return h end $all['g1-a']['g2-a'] << '1' $all['g1-a']['g2-a'] << '2' $all['g1-a']['g2-a'] << '3' $all['g1-a']['g2-b'] << '4' $all['g1-b']['g2-a'] << '5' $all['g1-b']['g2-c'] << '6' $all.keys.each do |group1| $all[group1].keys.each do |group2| $all[group1][group2].each do |item| puts "#{group1} #{group2} #{item}" end end end 

El resultado es:

 $ ruby -v && ruby t.rb ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux] g1-a g2-a 1 g1-a g2-a 2 g1-a g2-a 3 g1-a g2-b 4 g1-b g2-a 5 g1-b g2-c 6 

Actualmente estoy probando esto:

 # -------------------------------------------------------------------- # System so that we chain methods together without worrying about nil # values (a la Objective-c). # Example: # params[:foo].try?[:bar] # class Object # Returns self, unless NilClass (see below) def try? self end end class NilClass class MethodMissingSink include Singleton def method_missing(meth, *args, &block) end end def try? MethodMissingSink.instance end end 

Conozco los argumentos en contra de la try , pero es útil cuando se trata de cosas, como por ejemplo, params .