Compréhension de liste en Ruby


93

Pour faire l'équivalent de la compréhension de liste Python, je fais ce qui suit:

some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}

Y a-t-il une meilleure façon de faire cela ... peut-être avec un seul appel de méthode?


3
La vôtre et les réponses de Glenn McDonald me semblent bonnes ... Je ne vois pas ce que vous gagneriez en essayant d'être plus concis que les deux.
Pistos

1
cette solution traverse la liste deux fois. L'injection ne le fait pas.
Pedro Rolo

2
Quelques réponses géniales ici, mais ce serait génial aussi de voir des idées pour la compréhension de la liste dans plusieurs collections.
Bo Jeanes

Réponses:


55

Si vous le souhaitez vraiment, vous pouvez créer une méthode Array # comprehend comme celle-ci:

class Array
  def comprehend(&block)
    return self if block.nil?
    self.collect(&block).compact
  end
end

some_array = [1, 2, 3, 4, 5, 6]
new_array = some_array.comprehend {|x| x * 3 if x % 2 == 0}
puts new_array

Impressions:

6
12
18

Je le ferais probablement comme vous l'avez fait.


2
Vous pouvez utiliser compact! pour optimiser un peu
Alexey

9
Ce n'est pas vraiment correct, considérez: [nil, nil, nil].comprehend {|x| x }qui retourne [].
trente

alexey, selon la documentation, compact!renvoie nil au lieu du tableau lorsqu'aucun élément n'est modifié, donc je ne pense pas que cela fonctionne.
Binary Phile

89

Que diriez-vous:

some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact

Légèrement plus propre, du moins à mon goût, et selon un rapide test de référence environ 15% plus rapide que votre version ...


4
ainsi que some_array.map{|x| x * 3 unless x % 2}.compact, qui est sans doute plus lisible / rubis-esque.
piscine nocturne

5
@nightpool unless x%2n'a aucun effet puisque 0 est la vérité en ruby. Voir: gist.github.com/jfarmer/2647362
Abhinav Srivastava

30

J'ai fait une comparaison rapide des trois alternatives et la carte compacte semble vraiment être la meilleure option.

Test de performance (rails)

require 'test_helper'
require 'performance_test_help'

class ListComprehensionTest < ActionController::PerformanceTest

  TEST_ARRAY = (1..100).to_a

  def test_map_compact
    1000.times do
      TEST_ARRAY.map{|x| x % 2 == 0 ? x * 3 : nil}.compact
    end
  end

  def test_select_map
    1000.times do
      TEST_ARRAY.select{|x| x % 2 == 0 }.map{|x| x * 3}
    end
  end

  def test_inject
    1000.times do
      TEST_ARRAY.inject([]) {|all, x| all << x*3 if x % 2 == 0; all }
    end
  end

end

Résultats

/usr/bin/ruby1.8 -I"lib:test" "/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/performance/list_comprehension_test.rb" -- --benchmark
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
ListComprehensionTest#test_inject (1230 ms warmup)
           wall_time: 1221 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_map_compact (860 ms warmup)
           wall_time: 855 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.ListComprehensionTest#test_select_map (961 ms warmup)
           wall_time: 955 ms
              memory: 0.00 KB
             objects: 0
             gc_runs: 0
             gc_time: 0 ms
.
Finished in 66.683039 seconds.

15 tests, 0 assertions, 0 failures, 0 errors

1
Il serait également intéressant de voir reducece benchmark (voir stackoverflow.com/a/17703276 ).
Adam Lindberg

3
inject==reduce
ben.snape

map_compact peut-être plus vite mais il crée un nouveau tableau. inject est peu encombrant que map.compact et select.map
bibstha

11

Il semble y avoir une certaine confusion parmi les programmeurs Ruby dans ce fil concernant ce qu'est la compréhension de liste. Chaque réponse suppose un tableau préexistant à transformer. Mais le pouvoir de la compréhension de liste réside dans un tableau créé à la volée avec la syntaxe suivante:

squares = [x**2 for x in range(10)]

Ce qui suit serait un analogue de Ruby (la seule réponse adéquate dans ce fil, AFAIC):

a = Array.new(4).map{rand(2**49..2**50)} 

Dans le cas ci-dessus, je crée un tableau d'entiers aléatoires, mais le bloc peut contenir n'importe quoi. Mais ce serait une compréhension de la liste Ruby.


1
Comment feriez-vous ce que le PO essaie de faire?
Andrew Grimm

2
En fait, je vois maintenant que le PO lui-même avait une liste existante que l'auteur voulait transformer. Mais la conception archétypale de la compréhension de liste implique la création d'un tableau / liste là où il n'en existait pas auparavant en référençant une itération. Mais en fait, certaines définitions formelles disent que la compréhension de liste ne peut pas du tout utiliser la carte, donc même ma version n'est pas casher - mais aussi proche que possible avec Ruby, je suppose.
Mark

5
Je ne comprends pas comment votre exemple Ruby est censé être un analogue de votre exemple Python. Le code Ruby doit lire: squares = (0..9) .map {| x | x ** 2}
michau

4
Bien que @michau ait raison, tout l'intérêt de la compréhension de liste (que Mark a négligé) est que la compréhension de liste elle-même n'utilise pas de génération de tableaux - elle utilise des générateurs et des co-routines pour faire tous les calculs en continu sans allouer du tout de stockage (sauf variables temporaires) jusqu'à ce que (ssi) les résultats atterrissent dans une variable de tableau - c'est le but des crochets dans l'exemple python, pour réduire la compréhension à un ensemble de résultats. Ruby n'a aucune installation similaire aux générateurs.
Guss

4
Oh oui, il a (depuis Ruby 2.0): squares_of_all_natural_numbers = (0..Float :: INFINITY) .lazy.map {| x | x ** 2}; p squares_of_all_natural_numbers.take (10) .to_a
michau

11

J'ai discuté de ce sujet avec Rein Henrichs, qui me dit que la solution la plus performante est

map { ... }.compact

Cela a du bon sens car cela évite de créer des tableaux intermédiaires comme avec l'utilisation immuable de Enumerable#inject, et cela évite de développer le tableau, ce qui provoque une allocation. C'est aussi général que n'importe lequel des autres, sauf si votre collection peut contenir des éléments nuls.

Je n'ai pas comparé ça avec

select {...}.map{...}

Il est possible que l'implémentation en C de Ruby Enumerable#selectsoit également très bonne.


9

Une solution alternative qui fonctionnera dans chaque implémentation et fonctionnera en temps O (n) au lieu de O (2n) est:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}

11
Vous voulez dire qu'il parcourt la liste une seule fois. Si vous utilisez la définition formelle, O (n) est égal à O (2n). Just nitpicking :)
Daniel Hepper

1
@Daniel Harper :) Non seulement vous avez raison, mais aussi pour le cas moyen, traverser la liste une fois pour supprimer certaines entrées, puis à nouveau pour effectuer une opération peut être effectivement mieux dans les cas moyens :)
Pedro Rolo

En d' autres termes, vous faites des 2choses nfois au lieu de 1chose nfois, puis une autre 1chose nfois :) Un avantage important de inject/ reduceest qu'il conserve toutes les nilvaleurs dans la séquence d'entrée qui est plus le comportement de liste comprehensionly
John La Rooy

8

Je viens de publier le joyau de compréhension sur RubyGems, ce qui vous permet de faire ceci:

require 'comprehend'

some_array.comprehend{ |x| x * 3 if x % 2 == 0 }

C'est écrit en C; le tableau n'est parcouru qu'une seule fois.


7

Enumerable a une grepméthode dont le premier argument peut être un prédicat proc, et dont le deuxième argument facultatif est une fonction de mappage; donc ce qui suit fonctionne:

some_array.grep(proc {|x| x % 2 == 0}) {|x| x*3}

Ce n'est pas aussi lisible que quelques autres suggestions (j'aime le joyau simple select.mapou compréhensible de l'histocrate d'anoiaque), mais ses points forts sont qu'il fait déjà partie de la bibliothèque standard, et qu'il est en un seul passage et n'implique pas la création de tableaux intermédiaires temporaires , et ne nécessite pas de valeur hors limites comme celle nilutilisée dans les compactsuggestions -using.


4

C'est plus concis:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}

2
Ou, pour encore plus de génialité sans point[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Jörg W Mittag

4
[1, 2, 3, 4, 5, 6].collect{|x| x * 3 if x % 2 == 0}.compact
=> [6, 12, 18]

Ça marche pour moi. C'est aussi propre. Oui, c'est la même chose que map, mais je pense que cela collectrend le code plus compréhensible.


select(&:even?).map()

semble vraiment mieux, après l'avoir vu ci-dessous.


2

Comme Pedro l'a mentionné, vous pouvez fusionner les appels chaînés vers Enumerable#selectet Enumerable#map, en évitant une traversée des éléments sélectionnés. Cela est vrai car il Enumerable#selects'agit d'une spécialisation de pli ou inject. J'ai posté une introduction hâtive au sujet dans le sous-répertoire Ruby.

La fusion manuelle des transformations Array peut être fastidieuse, alors peut-être que quelqu'un pourrait jouer avec l' comprehendimplémentation de Robert Gamble pour rendre ce select/ mappattern plus joli.


2

Quelque chose comme ça:

def lazy(collection, &blk)
   collection.map{|x| blk.call(x)}.compact
end

Appeler:

lazy (1..6){|x| x * 3 if x.even?}

Qui renvoie:

=> [6, 12, 18]

Quel est le problème avec la définition lazysur Array et ensuite:(1..6).lazy{|x|x*3 if x.even?}
Guss

1

Une autre solution mais peut-être pas la meilleure

some_array.flat_map {|x| x % 2 == 0 ? [x * 3] : [] }

ou

some_array.each_with_object([]) {|x, list| x % 2 == 0 ? list.push(x * 3) : nil }

0

Voici une façon d'aborder ceci:

c = -> x do $*.clear             
  if x['if'] && x[0] != 'f' .  
    y = x[0...x.index('for')]    
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif x['if'] && x[0] == 'f'
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << x")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  elsif !x['if'] && x[0] != 'f'
    y = x[0...x.index('for')]
    x = x[x.index('for')..-1]
    (x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
    x.insert(x.length, "end; $*")
    eval(x)
    $*)
  else
    eval(x.split[3]).to_a
  end
end 

Donc, fondamentalement, nous convertissons une chaîne en syntaxe ruby ​​appropriée pour la boucle, puis nous pouvons utiliser la syntaxe python dans une chaîne à faire:

c['for x in 1..10']
c['for x in 1..10 if x.even?']
c['x**2 for x in 1..10 if x.even?']
c['x**2 for x in 1..10']

# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# [2, 4, 6, 8, 10]
# [4, 16, 36, 64, 100]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

ou si vous n'aimez pas l'apparence de la chaîne ou si vous devez utiliser un lambda, nous pourrions renoncer à essayer de refléter la syntaxe python et faire quelque chose comme ceci:

S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]

0

Ruby 2.7 introduit filter_mapqui réalise à peu près ce que vous voulez (carte + compact):

some_array.filter_map { |x| x * 3 if x % 2 == 0 }

Vous pouvez en savoir plus ici .



-4

Je pense que la plus grande compréhension de la liste serait la suivante:

some_array.select{ |x| x * 3 if x % 2 == 0 }

Puisque Ruby nous permet de placer le conditionnel après l'expression, nous obtenons une syntaxe similaire à la version Python de la compréhension de liste. De plus, comme la selectméthode n'inclut rien qui équivaut à false, toutes les valeurs nulles sont supprimées de la liste résultante et aucun appel à compact n'est nécessaire comme ce serait le cas si nous avions utilisé mapou à la collectplace.


7
Cela ne semble pas fonctionner. Au moins dans Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 si x% 2 == 0} évalue à [2, 4, 6] Enumerable # select se soucie uniquement de savoir si le bloc évalue à vrai ou faux, pas à la valeur qu'il produit, AFAIK.
Greg Campbell
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.