Rozumienie listy w Rubim


93

Aby zrobić odpowiednik list składanych w Pythonie, wykonuję następujące czynności:

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

Czy jest lepszy sposób na zrobienie tego ... może za pomocą jednego wywołania metody?


3
Zarówno twoje odpowiedzi, jak i odpowiedzi Glenna McDonalda wydają mi się w porządku ... Nie widzę, co byś zyskał, starając się być bardziej zwięzłym.
Pistos

1
to rozwiązanie przecina listę dwukrotnie. Wstrzyknięcie nie.
Pedro Rolo

2
Oto kilka niesamowitych odpowiedzi, ale byłoby też niesamowite, zobaczyć pomysły na zrozumienie listy w wielu kolekcjach.
Bo Jeanes

Odpowiedzi:


55

Jeśli naprawdę chcesz, możesz utworzyć metodę Array # comprehend w następujący sposób:

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

Wydruki:

6
12
18

Jednak prawdopodobnie zrobiłbym to tak, jak ty.


2
Możesz użyć kompaktowego! trochę zoptymalizować
Alexey

9
To nie jest w rzeczywistości poprawne, rozważ: [nil, nil, nil].comprehend {|x| x }który zwraca [].
Ted Kaplan,

alexey, zgodnie z dokumentacją, compact!zwraca nil zamiast tablicy, gdy żadne elementy nie są zmieniane, więc nie sądzę, aby to działało.
Plik binarny

89

Może:

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

Nieco czystszy, przynajmniej jak na mój gust, i według szybkiego testu porównawczego około 15% szybciej niż twoja wersja ...


4
jak również some_array.map{|x| x * 3 unless x % 2}.compact, który jest prawdopodobnie bardziej czytelny / rubinowy.
basen nocny

5
@nightpool unless x%2nie ma żadnego efektu, ponieważ 0 jest prawdą w rubinie. Zobacz: gist.github.com/jfarmer/2647362
Abhinav Srivastava

30

Dokonałem szybkiego testu porównawczego, porównując trzy alternatywy, a kompresja mapy naprawdę wydaje się być najlepszą opcją.

Test wydajności (szyny)

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

Wyniki

/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
Byłoby ciekawie zobaczyć również reducew tym teście porównawczym (patrz stackoverflow.com/a/17703276 ).
Adam Lindberg,

3
inject==reduce
ben.snape

map_compact może być szybsze, ale tworzy nową tablicę. inject jest bardziej pojemny niż map.compact i select.map
bibstha

11

Wydaje się, że wśród programistów Rubiego jest pewne zamieszanie w tym wątku, co do tego, czym jest rozumienie list. Każda odpowiedź zakłada przekształcenie jakiejś istniejącej tablicy. Ale siła rozumienia list tkwi w tablicy tworzonej w locie o następującej składni:

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

Poniższy przykład byłby analogiem w Rubim (jedyna odpowiednia odpowiedź w tym wątku, AFAIC):

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

W powyższym przypadku tworzę tablicę losowych liczb całkowitych, ale blok może zawierać wszystko. Ale to byłoby zrozumienie listy Ruby.


1
Jak byś zrobił to, co próbuje zrobić OP?
Andrew Grimm

2
Właściwie widzę, że sam PO miał jakąś istniejącą listę, którą autor chciał przekształcić. Ale archetypowa koncepcja rozumienia list polega na utworzeniu tablicy / listy, której wcześniej nie było, poprzez odwołanie się do jakiejś iteracji. Ale w rzeczywistości niektóre formalne definicje mówią, że rozumienie list nie może w ogóle używać mapy, więc nawet moja wersja nie jest koszerna - ale chyba tak bliska, jak można to uzyskać w Rubim.
Mark

5
Nie rozumiem, w jaki sposób Twój przykład w Rubim powinien być analogiem Twojego przykładu w Pythonie. Kod Ruby powinien brzmieć: squares = (0..9) .map {| x | x ** 2}
michau

4
Chociaż @michau ma rację, cały punkt rozumienia listy (który Mark zaniedbał) polega na tym, że samo rozumienie list nie używa nie generowania tablic - używa generatorów i współprogramów do wykonywania wszystkich obliczeń w sposób strumieniowy bez alokowania pamięci zmienne temp), aż (iff) wyniki trafią do zmiennej tablicowej - to jest celem nawiasów kwadratowych w przykładzie w Pythonie, aby zwinąć rozumienie do zestawu wyników. Ruby nie ma funkcji podobnej do generatorów.
Guss

4
O tak, ma (od 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

Omówiłem ten temat z Reinem Henrichsem, który powiedział mi, że najlepszym rozwiązaniem jest

map { ... }.compact

Ma to sens, ponieważ unika tworzenia tablic pośrednich, jak w przypadku niezmiennego użycia Enumerable#inject, i unika powiększania tablicy, co powoduje alokację. Jest tak ogólna, jak każda inna, chyba że Twoja kolekcja może zawierać zero elementów.

Nie porównałem tego z

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

Możliwe, że implementacja języka Ruby w C Enumerable#selectjest również bardzo dobra.


9

Alternatywnym rozwiązaniem, które sprawdzi się w każdej implementacji i będzie działało w czasie O (n) zamiast O (2n) jest:

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

11
Masz na myśli to, że przechodzi przez listę tylko raz. Jeśli postępujesz zgodnie z formalną definicją, O (n) równa się O (2n). Po prostu czepiam się :)
Daniel Hepper

1
@Daniel Harper :) Nie tylko masz rację, ale także w przeciętnym przypadku, przekrojenie listy raz, aby odrzucić niektóre wpisy, a następnie ponownie, aby wykonać operację, może być właściwie lepsze w przeciętnych przypadkach :)
Pedro Rolo

Innymi słowy, robisz 2rzeczy nrazy zamiast 1rzeczy nrazy, a potem inna 1rzecz nrazy :) Jedną z ważnych zalet inject/ reducejest to, że zachowuje wszelkie nilwartości w sekwencji wejściowej, co jest zachowaniem bardziej zrozumiałym dla listy
John La Rooy

8

Właśnie opublikowałem comprehend gem w RubyGems, który pozwala ci to zrobić:

require 'comprehend'

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

Jest napisany w C; tablica jest przeszukiwana tylko raz.


7

Enumerable ma grepmetodę, której pierwszym argumentem może być predykat proc, a opcjonalnym drugim argumentem jest funkcja mapująca ; więc działa:

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

To nie jest tak czytelne, jak kilka innych sugestii (lubię prosty select.mapanoiaque lub zrozumiały klejnot histokraty), ale jego mocne strony polegają na tym, że jest już częścią standardowej biblioteki i jest jednoprzebiegowe i nie obejmuje tworzenia tymczasowych tablic pośrednich i nie wymaga wartości spoza zakresu, takiej jak nilużywana w compactsugestiach -using.


4

To jest bardziej zwięzłe:

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

2
Lub, dla jeszcze większej niesamowitości bez punktów[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]

To działa dla mnie. Jest również czysty. Tak, to to samo co map, ale myślę, collectże kod jest bardziej zrozumiały.


select(&:even?).map()

faktycznie wygląda lepiej, po obejrzeniu tego poniżej.


2

Jak wspomniał Pedro, możesz łączyć ze sobą połączone wywołania do Enumerable#selecti Enumerable#map, unikając przechodzenia przez wybrane elementy. Jest to prawdą, ponieważ Enumerable#selectjest to specjalizacja fold lub inject. Opublikowałem pośpieszne wprowadzenie do tematu na subreddicie Ruby.

Ręcznie zgrzewania transformacje tablic może być uciążliwe, więc może ktoś mógłby grać z Roberta Gamble comprehendrealizacji uczynienia tego select/ mapwzór ładniejsza.


2

Coś takiego:

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

Nazwać:

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

Który zwraca:

=> [6, 12, 18]

Co jest złego w definiowaniu lazyw Array, a następnie:(1..6).lazy{|x|x*3 if x.even?}
Guss

1

Kolejne rozwiązanie, ale może nie najlepsze

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

lub

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

0

Oto jeden ze sposobów rozwiązania tego problemu:

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 

więc w zasadzie konwertujemy ciąg na odpowiednią składnię ruby ​​dla pętli, a następnie możemy użyć składni Pythona w ciągu, aby wykonać:

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]

lub jeśli nie podoba ci się wygląd łańcucha lub konieczność użycia lambdy, możemy zrezygnować z próby odzwierciedlenia składni Pythona i zrobić coś takiego:

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

0

Ruby 2.7 wprowadził, filter_mapktóry prawie osiąga to, czego chcesz (mapa + kompakt):

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

Więcej na ten temat przeczytasz tutaj .



-4

Myślę, że najbardziej rozwinięta lista ze zrozumieniem wyglądałaby następująco:

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

Ponieważ Ruby pozwala nam umieścić warunek po wyrażeniu, otrzymujemy składnię podobną do wersji list składanych w Pythonie. Ponadto, ponieważ selectmetoda nie zawiera niczego, co jest równoważne false, wszystkie wartości zerowe są usuwane z listy wynikowej i nie jest konieczne wywołanie metody compact, jak byłoby to w przypadku, gdybyśmy użyli maplub collectzamiast tego.


7
To nie wydaje się działać. Przynajmniej w Rubim 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3, jeśli x% 2 == 0} daje w wyniku [2, 4, 6] Enumerable # select dba tylko o to, czy blok ma wartość true, czy false, a nie jaką wartość wyświetla, AFAIK.
Greg Campbell
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.