Listenverständnis in Ruby

93

Um das Äquivalent zum Verständnis der Python-Liste zu erreichen, gehe ich wie folgt vor:

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

Gibt es einen besseren Weg, dies zu tun ... vielleicht mit einem Methodenaufruf?

Schreibgeschützt
quelle
3
Sowohl Ihre als auch Glenn McDonalds Antworten scheinen mir in Ordnung zu sein ... Ich sehe nicht, was Sie gewinnen würden, wenn Sie versuchen würden, prägnanter zu sein als beide.
Pistos
1
Diese Lösung durchläuft die Liste zweimal. Die Injektion funktioniert nicht.
Pedro Rolo
2
Einige großartige Antworten hier, aber es wäre auch großartig, Ideen für das Listenverständnis in mehreren Sammlungen zu sehen.
Bo Jeanes

Antworten:

55

Wenn Sie wirklich möchten, können Sie eine Array # -Verständnismethode wie folgt erstellen:

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

Drucke:

6
12
18

Ich würde es wahrscheinlich genauso machen, wie du es getan hast.

Robert Gamble
quelle
2
Sie könnten kompakt verwenden! ein bisschen zu optimieren
Alexey
9
Dies ist nicht wirklich richtig, bedenken Sie: [nil, nil, nil].comprehend {|x| x }welche zurückgibt [].
siebenunddreißig
Laut den Dokumenten compact!gibt alexey nil anstelle des Arrays zurück, wenn keine Elemente geändert werden. Ich denke also nicht, dass dies funktioniert.
Binary Phile
89

Wie wäre es mit:

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

Etwas sauberer, zumindest nach meinem Geschmack, und laut einem schnellen Benchmark-Test etwa 15% schneller als Ihre Version ...

Glenn McDonald
quelle
4
sowie some_array.map{|x| x * 3 unless x % 2}.compact, was wohl besser lesbar / rubinartig ist.
Nachtpool
5
@nightpool unless x%2hat keine Auswirkung, da 0 in Ruby wahr ist. Siehe: gist.github.com/jfarmer/2647362
Abhinav Srivastava
30

Ich habe einen schnellen Benchmark erstellt, bei dem die drei Alternativen mit Map-Compact verglichen wurden scheint wirklich die beste Option zu sein.

Leistungstest (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

Ergebnisse

/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
Knuton
quelle
1
Es wäre interessant, dies auch reducein diesem Benchmark zu sehen (siehe stackoverflow.com/a/17703276 ).
Adam Lindberg
3
inject==reduce
ben.snape
map_compact ist vielleicht schneller, aber es wird ein neues Array erstellt. injizieren ist platzsparender als map.compact und select.map
bibstha
11

In diesem Thread scheint es unter Ruby-Programmierern einige Verwirrung darüber zu geben, was Listenverständnis ist. Jede einzelne Antwort setzt voraus, dass ein bereits vorhandenes Array transformiert wird. Die Stärke des Listenverständnisses liegt jedoch in einem Array, das im laufenden Betrieb mit der folgenden Syntax erstellt wird:

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

Das Folgende wäre ein Analogon in Ruby (die einzig angemessene Antwort in diesem Thread, AFAIC):

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

Im obigen Fall erstelle ich ein Array von zufälligen Ganzzahlen, aber der Block kann alles enthalten. Dies wäre jedoch ein Verständnis der Ruby-Liste.

Kennzeichen
quelle
1
Wie würden Sie tun, was das OP versucht?
Andrew Grimm
2
Eigentlich sehe ich jetzt, dass das OP selbst eine vorhandene Liste hatte, die der Autor transformieren wollte. Die archetypische Konzeption des Listenverständnisses besteht jedoch darin, ein Array / eine Liste zu erstellen, in der es zuvor keine gab, indem auf eine Iteration verwiesen wird. Aber tatsächlich sagen einige formale Definitionen, dass das Listenverständnis überhaupt keine Karte verwenden kann, so dass selbst meine Version nicht koscher ist - aber so nah, wie man es in Ruby erreichen könnte, denke ich.
Mark
5
Ich verstehe nicht, wie Ihr Ruby-Beispiel ein Analogon zu Ihrem Python-Beispiel sein soll. Der Ruby-Code sollte lauten: squares = (0..9) .map {| x | x ** 2}
Michau
4
Während @michau Recht hat, ist der springende Punkt beim Listenverständnis (das Mark vernachlässigt hat), dass das Listenverständnis selbst keine generierten Arrays verwendet - es verwendet Generatoren und Co-Routinen, um alle Berechnungen auf Streaming-Weise durchzuführen, ohne überhaupt Speicher zuzuweisen (außer temporäre Variablen), bis (iff) die Ergebnisse in einer Array-Variablen landen - dies ist der Zweck der eckigen Klammern im Python-Beispiel, um das Verständnis auf eine Reihe von Ergebnissen zu reduzieren. Ruby hat keine ähnliche Funktion wie Generatoren.
Guss
4
Oh ja, es hat (seit 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

Ich habe dieses Thema mit Rein Henrichs besprochen, der mir sagt, dass die Lösung mit der besten Leistung ist

map { ... }.compact

Dies ist sinnvoll, da das Erstellen von Zwischen-Arrays wie bei der unveränderlichen Verwendung von Enumerable#injectvermieden wird und das Array nicht vergrößert wird, was zu einer Zuweisung führt. Es ist so allgemein wie alle anderen, es sei denn, Ihre Sammlung kann keine Elemente enthalten.

Ich habe das nicht mit verglichen

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

Es ist möglich, dass Rubys C-Implementierung Enumerable#selectauch sehr gut ist.

jvoorhis
quelle
9

Eine alternative Lösung, die in jeder Implementierung funktioniert und in O (n) anstelle von O (2n) ausgeführt wird, ist:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}
Pedro Rolo
quelle
11
Sie meinen, es durchläuft die Liste nur einmal. Wenn Sie sich an die formale Definition halten, ist O (n) gleich O (2n). Nur nitpicking :)
Daniel Hepper
1
@ Daniel Harper :) Nicht nur Sie haben Recht, sondern auch für den Durchschnittsfall kann es im Durchschnitt tatsächlich besser sein, die Liste einmal zu durchlaufen, um einige Einträge zu verwerfen und dann erneut eine Operation durchzuführen :)
Pedro Rolo
Mit anderen Worten, tun Sie 2Dinge nmal statt , 1was nmal und dann noch 1etwas nmal :) Ein wichtiger Vorteil von inject/ reduceist , dass es keine bewahrt nilWerte in der Eingangssequenz , die mehr list-comprehensionly Verhalten
John La Rooy
8

Ich habe gerade das Comprehens-Juwel in RubyGems veröffentlicht, mit dem Sie Folgendes tun können:

require 'comprehend'

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

Es ist in C geschrieben; Das Array wird nur einmal durchlaufen.

Histokrat
quelle
7

Enumerable verfügt über eine grepMethode, deren erstes Argument ein Prädikat proc sein kann und deren optionales zweites Argument eine Zuordnungsfunktion ist. so funktioniert folgendes:

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

Dies ist nicht so lesbar wie einige andere Vorschläge (ich mag Anoiaques einfaches select.mapoder histokratisches Juwel), aber seine Stärken sind, dass es bereits Teil der Standardbibliothek ist, in einem Durchgang und ohne temporäre Zwischenarrays und erfordert keinen Wert außerhalb der Grenzen, wie niler in den compactVorschlägen zur Verwendung verwendet wird.

Peter Moulder
quelle
4

Dies ist prägnanter:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
anoiaque
quelle
2
Oder für noch mehr punktfreie Attraktivität[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]

Das ist für mich in Ordnung. Es ist auch sauber. Ja, es ist das gleiche wie map, aber ich denke, collectmacht den Code verständlicher.


select(&:even?).map()

sieht eigentlich besser aus, nachdem ich es unten gesehen habe.

Vince
quelle
2

Wie Pedro bereits erwähnt hat, können Sie die verketteten Anrufe mit Enumerable#selectund zusammenführen Enumerable#map, um ein Durchlaufen der ausgewählten Elemente zu vermeiden. Dies ist wahr, weil Enumerable#selecteine Spezialisierung von Falte oder inject. Ich habe eine hastige Einführung gepostet in das Thema im Ruby-Subreddit veröffentlicht.

Das manuelle Zusammenführen von Array-Transformationen kann mühsam sein. Vielleicht könnte jemand mit der comprehendImplementierung von Robert Gamble spielen , um dieses select/ mapMuster schöner zu machen .

jvoorhis
quelle
2

Etwas wie das:

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

Nennen:

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

Welches kehrt zurück:

=> [6, 12, 18]
Alexandre Magro
quelle
Was ist falsch daran, lazyauf Array zu definieren und dann:(1..6).lazy{|x|x*3 if x.even?}
Guss
1

Eine andere Lösung, aber vielleicht nicht die beste

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

oder

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

Dies ist eine Möglichkeit, dies zu erreichen:

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 

Im Grunde genommen konvertieren wir einen String in die richtige Ruby-Syntax für die Schleife. Dann können wir die Python-Syntax in einem String verwenden, um Folgendes zu tun:

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]

oder wenn Ihnen das Aussehen der Zeichenfolge nicht gefällt oder Sie kein Lambda verwenden müssen, können Sie auf den Versuch verzichten, die Python-Syntax zu spiegeln und so etwas zu tun:

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

Ruby 2.7 wurde eingeführt, filter_mapdas so ziemlich das erreicht, was Sie wollen (Karte + Kompakt):

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

Sie können mehr darüber lesen Sie hier .

Matheus Richard
quelle
0

https://rubygems.org/gems/ruby_list_comprehension

Schamloser Plug für mein Ruby List Comprehension-Juwel, um idiomatisches Ruby List-Verständnis zu ermöglichen

$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]
Sam Michael
quelle
-4

Ich denke, das Listenverständnis wäre das Folgende:

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

Da Ruby es uns ermöglicht, die Bedingung nach dem Ausdruck zu platzieren, erhalten wir eine Syntax ähnlich der Python-Version des Listenverständnisses. Da die selectMethode nichts enthält, was gleichbedeutend ist false, werden alle Nullwerte aus der resultierenden Liste entfernt, und es ist kein Aufruf von compact erforderlich, wie dies der Fall wäre, wenn wir mapoder collectstattdessen verwendet hätten.

Christopher Roach
quelle
7
Dies scheint nicht zu funktionieren. Zumindest in Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3, wenn x% 2 == 0} zu [2, 4, 6] ausgewertet wird. Enumerable # select kümmert sich nur darum, ob der Block als wahr oder falsch ausgewertet wird, nicht um den Wert, den er ausgibt, AFAIK.
Greg Campbell