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?
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?
Odpowiedzi:
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.
[nil, nil, nil].comprehend {|x| x }
który zwraca []
.
compact!
zwraca nil zamiast tablicy, gdy żadne elementy nie są zmieniane, więc nie sądzę, aby to działało.
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 ...
some_array.map{|x| x * 3 unless x % 2}.compact
, który jest prawdopodobnie bardziej czytelny / rubinowy.
unless x%2
nie ma żadnego efektu, ponieważ 0 jest prawdą w rubinie. Zobacz: gist.github.com/jfarmer/2647362
Dokonałem szybkiego testu porównawczego, porównując trzy alternatywy, a kompresja mapy naprawdę wydaje się być najlepszą opcją.
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
/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
reduce
w tym teście porównawczym (patrz stackoverflow.com/a/17703276 ).
inject
==reduce
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.
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#select
jest również bardzo dobra.
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}
2
rzeczy n
razy zamiast 1
rzeczy n
razy, a potem inna 1
rzecz n
razy :) Jedną z ważnych zalet inject
/ reduce
jest to, że zachowuje wszelkie nil
wartości w sekwencji wejściowej, co jest zachowaniem bardziej zrozumiałym dla listy
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.
Enumerable ma grep
metodę, 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.map
anoiaque 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 nil
używana w compact
sugestiach -using.
To jest bardziej zwięzłe:
[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Jak wspomniał Pedro, możesz łączyć ze sobą połączone wywołania do Enumerable#select
i Enumerable#map
, unikając przechodzenia przez wybrane elementy. Jest to prawdą, ponieważ Enumerable#select
jest 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 comprehend
realizacji uczynienia tego select
/ map
wzór ładniejsza.
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]
lazy
w Array, a następnie:(1..6).lazy{|x|x*3 if x.even?}
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]
Ruby 2.7 wprowadził, filter_map
któ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 .
https://rubygems.org/gems/ruby_list_comprehension
bezwstydna wtyczka do mojego klejnotu Ruby List Compression, aby umożliwić idiomatyczne rozumienie list Ruby
$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]
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ż select
metoda 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 map
lub collect
zamiast tego.