Comprensione delle liste in Ruby


93

Per fare l'equivalente della comprensione degli elenchi di Python, sto facendo quanto segue:

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

C'è un modo migliore per farlo ... forse con una chiamata al metodo?


3
Sia la tua risposta che quella di Glenn McDonald mi sembrano a posto ... Non vedo cosa guadagneresti cercando di essere più conciso di nessuno dei due.
Pistos

1
questa soluzione attraversa l'elenco due volte. L'iniezione no.
Pedro Rolo

2
Alcune risposte fantastiche qui, ma sarebbe fantastico anche vedere idee per la comprensione di elenchi in più raccolte.
Bo Jeanes

Risposte:


55

Se vuoi davvero, puoi creare un metodo Array # comprendere come questo:

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

Stampe:

6
12
18

Probabilmente lo farei semplicemente come hai fatto tu.


2
Potresti usare compatto! per ottimizzare un po '
Alexey

9
Questo non è effettivamente corretto, considera: [nil, nil, nil].comprehend {|x| x }quale ritorna [].
trentasette

alexey, secondo i documenti, compact!restituisce zero invece dell'array quando nessun elemento viene modificato, quindi non penso che funzioni.
File binario

89

Che ne dici di:

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

Leggermente più pulito, almeno secondo i miei gusti, e secondo un rapido test di benchmark circa il 15% più veloce della tua versione ...


4
così come some_array.map{|x| x * 3 unless x % 2}.compact, che è probabilmente più leggibile / rubino.
nightpool

5
@nightpool unless x%2non ha effetto poiché 0 è vero in ruby. Vedi: gist.github.com/jfarmer/2647362
Abhinav Srivastava

30

Ho fatto un rapido benchmark confrontando le tre alternative e map-compact sembra davvero essere l'opzione migliore.

Test delle prestazioni (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

Risultati

/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
Sarebbe interessante vedere anche reducein questo benchmark (vedere stackoverflow.com/a/17703276 ).
Adam Lindberg

3
inject==reduce
ben.snape

map_compact forse più veloce ma sta creando un nuovo array. inject è efficiente in termini di spazio rispetto a map.compact e select.map
bibstha

11

Sembra esserci una certa confusione tra i programmatori Ruby in questo thread riguardo a cosa sia la comprensione delle liste. Ogni singola risposta presuppone una matrice preesistente da trasformare. Ma il potere della comprensione delle liste risiede in un array creato al volo con la seguente sintassi:

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

Il seguente sarebbe un analogo in Ruby (l'unica risposta adeguata in questo thread, AFAIC):

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

Nel caso precedente, sto creando un array di numeri interi casuali, ma il blocco potrebbe contenere qualsiasi cosa. Ma questa sarebbe una comprensione della lista Ruby.


1
Come faresti ciò che l'OP sta cercando di fare?
Andrew Grimm

2
In realtà ora vedo che l'OP stesso aveva una lista esistente che l'autore voleva trasformare. Ma la concezione archetipica della comprensione delle liste implica la creazione di un array / elenco dove prima non esisteva facendo riferimento a qualche iterazione. Ma in realtà, alcune definizioni formali dicono che la comprensione delle liste non può usare affatto map, quindi anche la mia versione non è kosher, ma immagino il più vicino possibile a Ruby.
Marco

5
Non capisco come il tuo esempio Ruby dovrebbe essere un analogo del tuo esempio Python. Il codice Ruby dovrebbe essere: squares = (0..9) .map {| x | x ** 2}
michau

4
Mentre @michau ha ragione, l'intero punto della comprensione della lista (che Mark ha trascurato) è che la comprensione della lista stessa non usa non genera array - usa generatori e co routine per fare tutti i calcoli in modo streaming senza allocare memoria (tranne variabili temporanee) fino a quando (iff) i risultati non arrivano in una variabile array: questo è lo scopo delle parentesi quadre nell'esempio di Python, per ridurre la comprensione a un insieme di risultati. Ruby non ha strutture simili ai generatori.
Guss

4
Oh sì, ha (da 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

Ho discusso questo argomento con Rein Henrichs, che mi dice che la soluzione più performante è

map { ... }.compact

Ciò ha senso perché evita la creazione di array intermedi come con l'utilizzo immutabile di Enumerable#injected evita la crescita dell'array, che causa l'allocazione. È generale come gli altri a meno che la tua raccolta non possa contenere elementi nulli.

Non l'ho confrontato con

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

È possibile che anche l'implementazione in C di Ruby Enumerable#selectsia molto buona.


9

Una soluzione alternativa che funzionerà in ogni implementazione e verrà eseguita in O (n) invece di O (2n) è:

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

11
Vuoi dire che attraversa l'elenco solo una volta. Se segui la definizione formale, O (n) è uguale a O (2n). Solo pignolo :)
Daniel Hepper

1
@Daniel Harper :) Non solo hai ragione, ma anche per il caso medio, attraversare l'elenco una volta per scartare alcune voci, e poi di nuovo per eseguire un'operazione può essere effettivamente migliore nei casi medi :)
Pedro Rolo

In altre parole, stai facendo le 2cose nvolte invece di 1cose nvolte e poi un'altra 1cosa nvolte :) Un importante vantaggio di inject/ reduceè che conserva tutti i nilvalori nella sequenza di input che è un comportamento più comprensibile per la lista
John La Rooy

8

Ho appena pubblicato la gemma di comprensione su RubyGems, che ti consente di farlo:

require 'comprehend'

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

È scritto in C; l'array viene attraversato una sola volta.


7

Enumerable ha un grepmetodo il cui primo argomento può essere un predicato proc e il cui secondo argomento opzionale è una funzione di mappatura; quindi il seguente funziona:

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

Questo non è leggibile come un paio di altri suggerimenti (mi piace la select.mapgemma di comprensione semplice o istocratica di anoiaque), ma i suoi punti di forza sono che fa già parte della libreria standard ed è single-pass e non comporta la creazione di array intermedi temporanei e non richiede un valore fuori limite come quello nilusato nei compactsuggerimenti -using.


4

Questo è più conciso:

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

2
Oppure, per ancora più suggestioni senza punti[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]

Per me va bene. È anche pulito. Sì, è uguale a map, ma penso che collectrenda il codice più comprensibile.


select(&:even?).map()

effettivamente sembra migliore, dopo averlo visto di seguito.


2

Come ha detto Pedro, puoi fondere insieme le chiamate concatenate a Enumerable#selecte Enumerable#map, evitando un attraversamento sugli elementi selezionati. Questo è vero perché Enumerable#selectè una specializzazione di piega o inject. Ho postato una frettolosa introduzione all'argomento nel subreddit di Ruby.

Fondere manualmente le trasformazioni di array può essere noioso, quindi forse qualcuno potrebbe giocare con l' comprehendimplementazione di Robert Gamble per rendere questo select/ mappattern più carino.


2

Qualcosa come questo:

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

Chiamalo:

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

Che ritorna:

=> [6, 12, 18]

Cosa c'è di sbagliato nel definire lazysu Array e poi:(1..6).lazy{|x|x*3 if x.even?}
Guss

1

Un'altra soluzione ma forse non la migliore

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

o

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

0

Questo è un modo per avvicinarsi a questo:

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 

quindi fondamentalmente stiamo convertendo una stringa nella corretta sintassi ruby ​​per loop, quindi possiamo usare la sintassi python in una stringa per fare:

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]

o se non ti piace il modo in cui appare la stringa o dover usare un lambda potremmo rinunciare al tentativo di rispecchiare la sintassi di Python e fare qualcosa del genere:

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

0

Ruby 2.7 ha introdotto filter_mapche praticamente raggiunge ciò che desideri (mappa + compatto):

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

Puoi leggere di più al riguardo qui .



-4

Penso che la più lista di comprensione sarebbe la seguente:

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

Poiché Ruby ci consente di posizionare il condizionale dopo l'espressione, otteniamo una sintassi simile alla versione Python della comprensione delle liste. Inoltre, poiché il selectmetodo non include nulla che sia uguale a false, tutti i valori nulli vengono rimossi dall'elenco risultante e non è necessaria alcuna chiamata a compact come sarebbe il caso se avessimo usato mapo collectinvece.


7
Questo non sembra funzionare. Almeno in Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 se x% 2 == 0} restituisce [2, 4, 6] Enumerable # select si preoccupa solo se il blocco restituisce vero o falso, non quale valore emette, AFAIK.
Greg Campbell,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.