web-dev-qa-db-fra.com

Compréhension de liste en rubis

Pour faire l'équivalent de la compréhension de liste Python, je procède comme suit:

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

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

82
Readonly

Si vous voulez vraiment, vous pouvez créer une méthode Array # comprehend comme ceci:

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 tu le faisais.

53
Robert Gamble

Comment

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

Un peu plus propre, du moins à mon goût, et selon un rapide test de performance environ 15% plus rapide que votre version ...

83
glenn mcdonald

J'ai fait un repère rapide en comparant les trois alternatives et map-compact 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
27
knuton

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

map { ... }.compact`

Cela a du sens car cela évite de construire des tableaux intermédiaires comme avec l'utilisation immuable de Enumerable#inject, et cela évite de développer le tableau, ce qui entraîne une allocation. C’est aussi général que les autres, à moins que votre collection ne puisse contenir aucun élément.

Je n'ai pas comparé cela avec

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

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

11
jvoorhis

Il semble y avoir une certaine confusion parmi les programmeurs de Ruby dans ce fil en ce qui concerne la compréhension des listes. Chaque réponse suppose la transformation d’un tableau préexistant. Mais le pouvoir de compréhension de la 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 en 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.

10
Mark

Une solution alternative qui fonctionnera dans chaque implémentation et s'exécutera dans O(n) au lieu de O(2n) est la suivante:

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

Je viens de publier le comprendre gem to RubyGems, 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 fois.

8
histocrat

Enumerable a une méthode grep dont le premier argument peut être un processus de prédicat et dont le second argument facultatif est une fonction de mappage; donc les travaux suivants:

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

Ce n'est pas aussi lisible que quelques suggestions (j'aime bien le simple select.map ou la gemme à comprendre de l'histocrate anoiaque), mais sa force réside dans le fait qu'elle fait déjà partie de la bibliothèque standard, qu'elle est en un seul passage et qu'elle ne nécessite pas la création d'un intermédiaire temporaire. tableaux, et ne nécessite pas une valeur hors limites comme nil utilisée dans les suggestions compact-.

7
Peter Moulder

C'est plus concis:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
4
anoiaque
[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 collect rend le code plus compréhensible. 


select(&:even?).map()

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

4
eyko

Comme Pedro l'a mentionné, vous pouvez fusionner les appels chaînés à Enumerable#select et Enumerable#map, en évitant une traversée des éléments sélectionnés. Cela est vrai car Enumerable#select est une spécialisation de fold ou inject. J'ai posté une introduction précipitée au sujet à la subreddit Ruby.

La fusion manuelle des transformations Array peut être fastidieuse. Quelqu'un pourrait donc jouer avec l'implémentation comprehend de Robert Gamble pour rendre ce modèle select/map plus joli.

2
jvoorhis

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 retourne:

=> [6, 12, 18]
2
Alexandre Magro

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
joegiralt