web-dev-qa-db-fra.com

Accès aux éléments des hachages imbriqués dans ruby

Je travaille un petit utilitaire écrit en Ruby qui utilise largement les hachages imbriqués. Actuellement, je vérifie l'accès aux éléments de hachage imbriqués comme suit:

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

Y a-t-il une meilleure manière de faire cela? Je voudrais pouvoir dire:

value = structure[:a][:b]

Et obtenez nil si: a n'est pas une clé dans structure, etc.

30
Paul Morie

La façon dont je le fais habituellement ces jours-ci est la suivante:

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

Cela vous donnera un hachage qui crée un nouveau hachage comme entrée pour une clé manquante, mais renvoie zéro pour le deuxième niveau de clé:

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

Vous pouvez l'imbriquer pour ajouter plusieurs couches qui peuvent être traitées de cette façon:

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

h['bar'] -> {}
h['tar']['zar'] -> {}
h['scar']['far']['mar'] -> nil

Vous pouvez également chaîner indéfiniment en utilisant le default_proc méthode:

h = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }

h['bar'] -> {}
h['tar']['star']['par'] -> {}

Le code ci-dessus crée un hachage dont le proc par défaut crée un nouveau hachage avec le même proc par défaut. Ainsi, un hachage créé comme valeur par défaut lors de la recherche d'une clé non vue aura le même comportement par défaut.

EDIT: Plus de détails

Les hachages Ruby vous permettent de contrôler la façon dont les valeurs par défaut sont créées lorsqu'une recherche se produit pour une nouvelle clé. Lorsqu'il est spécifié, ce comportement est encapsulé sous la forme d'un objet Proc et est accessible via default_proc et default_proc= méthodes. Le proc par défaut peut également être spécifié en passant un bloc à Hash.new .

Décomposons un peu ce code. Ce n'est pas un Ruby idiomatique, mais il est plus facile de le décomposer en plusieurs lignes:

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

La ligne 1 déclare une variable recursive_hash pour être un nouveau Hash et commence un bloc pour être recursive_hash's default_proc. Le bloc contient deux objets: h, qui est l'instance Hash sur laquelle la recherche de clé est effectuée, et k, la clé recherchée.

La ligne 2 définit la valeur par défaut dans le hachage sur une nouvelle instance Hash. Le comportement par défaut de ce hachage est fourni en passant un Proc créé à partir du default_proc du hachage dans lequel se produit la recherche; c'est-à-dire, le proc par défaut que le bloc lui-même définit.

Voici un exemple d'une session 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=>{}}

Lorsque le hachage à recursive_hash[:foo] a été créé, son default_proc a été fourni par recursive_hash's default_proc. Cela a deux effets:

  1. Le comportement par défaut pour recursive_hash[:foo] est le même que recursive_hash.
  2. Le comportement par défaut des hachages créés par recursive_hash[:foo]'s default_proc sera identique à recursive_hash.

Donc, en continuant à la CISR, nous obtenons ce qui suit:

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=>{}}}}
27
Paul Morie

Traditionnellement, vous deviez vraiment faire quelque chose comme ça:

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

Cependant, Ruby 2.3 a ajouté une méthode Hash#Dig qui rend cette façon plus gracieuse:

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

Il y a une gemme appelée Ruby_Dig qui corrigera cela à votre place.

38
DigitalRoss

Ruby 2.3.0 a introduit ne nouvelle méthode appelée Dig sur Hash et Array qui résout entièrement ce problème.

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

Il renvoie nil si la clé est manquante à n'importe quel niveau.

Si vous utilisez une version de Ruby antérieure à 2.3, vous pouvez utiliser le Ruby_Dig gem ou implémentez-le vous-même:

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
34
user513951

J'ai fait du rubygem pour ça. Essayez Vine .

Installer:

gem install Vine

Usage:

hash.access("a.b.c")
14
Cheng

Je pense que l'une des solutions les plus lisibles utilise 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
7
Javid Jamae
value = structure[:a][:b] rescue nil
3
fl00r

Solution 1

Je l'ai suggéré dans ma question avant:

class NilClass; def to_hash; {} end end

Hash#to_hash est déjà défini et renvoie self. Ensuite, vous pouvez faire:

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

Le to_hash garantit que vous obtenez un hachage vide lorsque la recherche de clé précédente échoue.

Solution2

Cette solution est similaire dans son esprit à mu est une réponse trop courte en ce qu'elle utilise une sous-classe, mais toujours quelque peu différente. Dans le cas où il n'y a pas de valeur pour une certaine clé, elle n'utilise pas de valeur par défaut, mais crée plutôt une valeur de hachage vide, de sorte qu'elle n'a pas le problème de confusion dans l'assignation que la réponse de DigitalRoss a, comme cela a été souligné par mu est trop court.

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

Il s'écarte cependant de la spécification donnée dans la question. Lorsqu'une clé non définie est donnée, elle renverra une instruction de hachage vide de nil.

p structure[:c] # => {}

Si vous créez une instance de ce NilFreeHash depuis le début et affectez les valeurs-clés, cela fonctionnera, mais si vous souhaitez convertir un hachage en une instance de cette classe, cela peut être un problème.

2
sawa
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
1
Brian K

Cette fonction de patch de singe pour Hash devrait être la plus simple (au moins pour moi). Il ne modifie pas non plus la structure, c'est-à-dire le changement de nil en {}. Cela s'appliquerait également même si vous lisez un arbre à partir d'une source brute, par exemple JSON. Il n'a pas non plus besoin de produire des objets de hachage vides au fur et à mesure ou d'analyser une chaîne. rescue nil était en fait une bonne solution facile pour moi car je suis assez courageux pour un risque aussi faible, mais je trouve que cela a essentiellement un inconvénient avec les performances.

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

Exemple:

> structure = { :a => { :b => 'foo' }}
=> {:a=>{:b=>"foo"}}

> structure.recurse(:a, :b)
=> "foo"

> structure.recurse(:a, :x)
=> nil

Ce qui est également bien, c'est que vous pouvez jouer avec les tableaux enregistrés avec:

> keys = [:a, :b]
=> [:a, :b]

> structure.recurse(*keys)
=> "foo"

> structure.recurse(*keys, :x1, :x2)
=> nil
1
konsolebox

Vous pouvez simplement créer une sous-classe Hash avec une méthode variadique supplémentaire pour creuser tout le long avec des vérifications appropriées en cours de route. Quelque chose comme ça (avec un meilleur nom bien sûr):

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

Ensuite, utilisez simplement Things au lieu des hachages:

>> 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
1
mu is too short

J'essaye actuellement ceci:

# --------------------------------------------------------------------
# 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

Je connais les arguments contre try, mais c'est utile lorsque l'on regarde des choses, comme disons, params.

0
Jaime Cham

Dans mon cas, j'avais besoin d'une matrice à deux dimensions où chaque cellule est une liste d'éléments.

J'ai trouvé cette technique qui semble fonctionner. Cela pourrait fonctionner pour le PO:

$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

La sortie est:

$ 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
0
JohnA

Vous pouvez utiliser la gemme andand , mais je m'en méfie de plus en plus:

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

Il y a la façon mignonne mais mauvaise de le faire. Qui consiste à monkey-patch NilClass pour ajouter un [] méthode qui renvoie nil. Je dis que c'est la mauvaise approche car vous n'avez aucune idée de ce que d'autres logiciels peuvent avoir fait une version différente, ou quel changement de comportement dans une future version de Ruby peut être cassé par cela.

Une meilleure approche consiste à créer un nouvel objet qui fonctionne un peu comme nil mais prend en charge ce comportement. Faites de ce nouvel objet le retour par défaut de vos hachages. Et puis ça marchera.

Alternativement, vous pouvez créer une simple fonction de "recherche imbriquée" à laquelle vous passez le hachage et les clés, qui parcourt les hachages dans l'ordre, éclatant quand il le peut.

Je préférerais personnellement l'une des deux dernières approches. Bien que je pense que ce serait mignon si le premier était intégré dans le langage Ruby. (Mais le patch de singe est une mauvaise idée. Ne faites pas ça. Surtout pour ne pas démontrer ce qu'est un hacker cool) tu es.)

0
btilly

Non pas que je le ferais, mais vous pouvez Monkeypatch dans 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"

Allez avec la réponse de @ DigitalRoss. Oui, c'est plus de frappe, mais c'est parce que c'est plus sûr.

0
Phrogz