into - ruby puts hash
Accediendo a elementos de hash anidados en ruby (15)
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
.
Esta pregunta ya tiene una respuesta aquí:
Estoy trabajando en una pequeña utilidad escrita en ruby que hace un uso extensivo de hashes anidados. Actualmente, estoy verificando el acceso a los elementos hash anidados 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.
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
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
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
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).
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 rubí 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:
- El comportamiento predeterminado para
recursive_hash[:foo]
es el mismo querecursive_hash
. - El comportamiento predeterminado para los hashes creados por el pro ceso_de_recursos_recostivo
recursive_hash[:foo]
será el mismo querecursive_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=>{}}}}
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 `<main>''
> 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.
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
Puedes usar la joya andand gema, 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
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 gema 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
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 gema llamada ruby_dig
que hará un parche de respaldo para usted.
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.
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
value = structure[:a][:b] rescue nil