Щоб зробити еквівалент розуміння списку Python, я роблю наступне:
some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}
Чи є кращий спосіб зробити це ... можливо, за допомогою одного виклику методу?
Щоб зробити еквівалент розуміння списку Python, я роблю наступне:
some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}
Чи є кращий спосіб зробити це ... можливо, за допомогою одного виклику методу?
Відповіді:
Якщо ви дійсно хочете, ви можете створити метод Array # зразумети таким чином:
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
Друк:
6
12
18
Я, мабуть, просто зробив би це так, як ти це зробив.
[nil, nil, nil].comprehend {|x| x }
що повертається []
.
compact!
повертає nil замість масиву, коли жоден елемент не змінюється, тому я не думаю, що це працює.
Як щодо:
some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact
Трохи чистіший, принаймні на мій смак, і відповідно до швидкого тесту на 15% швидше, ніж ваша версія ...
some_array.map{|x| x * 3 unless x % 2}.compact
, що, можливо, є більш читабельним / рубіновим.
unless x%2
не має ефекту, оскільки 0 в рубіні неправдиве. Див .: gist.github.com/jfarmer/2647362
Я зробив швидкий орієнтир, порівнюючи три альтернативи, і компактна карта справді здається найкращим варіантом.
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
в цьому еталоні (див. Stackoverflow.com/a/17703276 ).
inject
==reduce
У цій темі серед програмістів Ruby існує певна плутанина щодо розуміння списку. Кожна відповідь передбачає перетворення певного існуючого масиву. Але сила розуміння списку полягає в масиві, створеному на льоту з таким синтаксисом:
squares = [x**2 for x in range(10)]
Наступне буде аналогом Ruby (єдина відповідна відповідь у цій темі, AFAIC):
a = Array.new(4).map{rand(2**49..2**50)}
У наведеному вище випадку я створюю масив випадкових цілих чисел, але блок може містити що завгодно. Але це було б розумінням списку Рубі.
Я обговорював цю тему з Рейном Генріхсом, який сказав мені, що найкращим рішенням є
map { ... }.compact
Це має гарний сенс, оскільки це дозволяє уникнути побудови проміжних масивів, як при незмінному використанні Enumerable#inject
, і уникає нарощування масиву, що спричиняє виділення. Це так само загально, як і будь-яка інша, якщо ваша колекція не може містити нульових елементів.
Я не порівнював це з
select {...}.map{...}
Цілком можливо, що реалізація Ruby C Enumerable#select
також дуже хороша.
Альтернативним рішенням, яке працюватиме у кожному впровадженні та працюватиме у O (n), а не в O (2n), є час:
some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}
2
речі n
разів замість разів речей 1
, n
а потім іншу 1
річ n
разів :) Однією важливою перевагою inject
/ reduce
є те, що вона зберігає будь-які nil
значення у вхідній послідовності, що є більшою поведінкою для розуміння списку
Я щойно опублікував розуміючий самоцвіт у RubyGems, що дозволяє вам зробити це:
require 'comprehend'
some_array.comprehend{ |x| x * 3 if x % 2 == 0 }
Це написано на C; масив проходить лише один раз.
Enumerable має grep
метод, перший аргумент якого може бути предикатом proc, а необов’язковий другий аргумент - функція відображення; отже, працює наступне:
some_array.grep(proc {|x| x % 2 == 0}) {|x| x*3}
Це не так читабельно, як пара інших пропозицій (мені подобається простий select.map
самоцвіт anoiaque або розуміння гістократа), але його сильні сторони полягають у тому, що він вже є частиною стандартної бібліотеки, є однопрохідним і не передбачає створення тимчасових проміжних масивів , і не вимагає значень, що виходять за межі, як це було nil
використано в compact
пропозиціях -use.
Це більш стисло:
[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Як згадував Педро, ви можете поєднати ланцюгові дзвінки до Enumerable#select
та Enumerable#map
, уникаючи обходу вибраних елементів. Це вірно, оскільки Enumerable#select
є спеціалізацією складання або inject
. Я опублікував поспішне вступ до цієї теми в subreddit Ruby.
Зміщення перетворень Array вручну може бути нудним, тому, можливо, хтось може пограти з реалізацією Роберта Гембла, comprehend
щоб зробити цей select
/ map
шаблон гарнішим.
Щось на зразок цього:
def lazy(collection, &blk)
collection.map{|x| blk.call(x)}.compact
end
Назви це:
lazy (1..6){|x| x * 3 if x.even?}
Що повертає:
=> [6, 12, 18]
lazy
на Array, а потім:(1..6).lazy{|x|x*3 if x.even?}
Це один із способів підійти до цього:
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
тому в основному ми перетворюємо рядок у правильний рубіновий синтаксис циклу, тоді ми можемо використовувати синтаксис python у рядку для виконання:
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]
або якщо вам не подобається те, як виглядає рядок, або коли потрібно використовувати лямбду, ми можемо відмовитись від спроби відобразити синтаксис python і зробити щось подібне:
S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]
Представлений Ruby 2.7, filter_map
який майже досягає бажаного (карта + компакт):
some_array.filter_map { |x| x * 3 if x % 2 == 0 }
Детальніше про це ви можете прочитати тут .
https://rubygems.org/gems/ruby_list_comprehension
безсоромна вилка для моєї дорогоцінної камені Ruby List Comprehension, щоб дозволити ідіоматичне розуміння списку Ruby
$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]
Я думаю, що найбільш зрозумілим для сприйняття списком буде наступне:
some_array.select{ |x| x * 3 if x % 2 == 0 }
Оскільки Ruby дозволяє нам поставити умовний символ після виразу, ми отримуємо синтаксис, подібний до версії Python для розуміння списку. Крім того, оскільки select
метод не включає нічого, що прирівнюється до false
, усі значення nil видаляються із результуючого списку, і жоден виклик компактування не потрібен, як це було б у випадку, якщо б ми використовували map
або collect
замість цього.