Понимание списков в Ruby

93

Чтобы сделать эквивалент понимания списков Python, я делаю следующее:

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

Есть ли лучший способ сделать это ... возможно, с помощью одного вызова метода?

Только чтение
источник
3
И ваши ответы, и ответы Гленна Макдональда кажутся мне хорошими ... Я не понимаю, что вы выиграете, если попытаетесь быть более краткими.
Pistos
1
это решение пересекает список два раза. Инъекция - нет.
Педро Роло,
2
Здесь есть отличные ответы, но было бы здорово также увидеть идеи для понимания списков в нескольких коллекциях.
Bo Jeanes

Ответы:

55

Если вы действительно хотите, вы можете создать метод Array # comprehend следующим образом:

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

Я бы, наверное, просто поступил так же, как ты.

Роберт Гэмбл
источник
2
Вы можете использовать компактный! чтобы немного оптимизировать
Алексей
9
На самом деле это не так, подумайте: [nil, nil, nil].comprehend {|x| x }который возвращается [].
Тед Каплан
alexey, согласно документации, compact!возвращает nil вместо массива, когда никакие элементы не меняются, поэтому я не думаю, что это работает.
Binary Phile
89

Как насчет:

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

Немного чище, по крайней мере, на мой вкус, и, согласно быстрому тесту, примерно на 15% быстрее, чем ваша версия ...

Гленн Макдональд
источник
4
а также some_array.map{|x| x * 3 unless x % 2}.compact, который, возможно, более читаемый / рубиновый.
nightpool
5
@nightpool unless x%2не действует, так как 0 в рубине является истинным. См .: gist.github.com/jfarmer/2647362
Абхинав Шривастава,
30

Я провел быстрый тест, сравнивая три альтернативы, и map-compact действительно кажется лучшим вариантом.

Тест производительности (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

Полученные результаты

/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
источник
1
Было бы интересно увидеть и reduceв этом тесте (см. Stackoverflow.com/a/17703276 ).
Адам Линдберг,
3
inject==reduce
ben.snape
map_compact может быть быстрее, но он создает новый массив. inject занимает
меньше
11

Похоже, что в этой ветке Ruby-программисты не понимают, что такое понимание списков. Каждый ответ предполагает преобразование некоторого уже существующего массива. Но сила понимания списка заключается в массиве, созданном на лету со следующим синтаксисом:

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

Следующее будет аналогом в Ruby (единственный адекватный ответ в этой ветке AFAIC):

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

В приведенном выше случае я создаю массив случайных целых чисел, но блок может содержать что угодно. Но это было бы понимание списка Ruby.

отметка
источник
1
Как бы вы сделали то, что пытается сделать ОП?
Эндрю Гримм
2
На самом деле теперь я вижу, что у самого OP был какой-то существующий список, который автор хотел преобразовать. Но архетипическая концепция понимания списков включает создание массива / списка там, где его раньше не было, путем ссылки на некоторую итерацию. Но на самом деле некоторые формальные определения говорят, что понимание списка вообще не может использовать карту, поэтому даже моя версия не кошерная - но я думаю, она настолько близка, насколько это возможно в Ruby.
Марк
5
Я не понимаю, как ваш пример Ruby должен быть аналогом вашего примера Python. Код Ruby должен выглядеть так: squares = (0..9) .map {| x | x ** 2}
michau 09
4
Хотя @michau прав, весь смысл понимания списка (которым пренебрегал Марк) состоит в том, что само понимание списка не использует массивы not generate - оно использует генераторы и co-процедуры для выполнения всех вычислений в потоковом режиме без выделения памяти вообще (кроме временные переменные) до тех пор, пока (если и только что) результаты не попадут в переменную массива - это цель квадратных скобок в примере Python, чтобы свернуть понимание до набора результатов. Ruby не имеет возможностей, подобных генераторам.
Guss
4
О да, есть (начиная с 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

Я обсуждал эту тему с Рейном Хенрихсом, который сказал мне, что наиболее эффективным решением является

map { ... }.compact

Это имеет смысл, поскольку позволяет избежать создания промежуточных массивов, как при неизменном использовании Enumerable#inject, и избежать роста массива, который вызывает выделение. Он такой же общий, как и любые другие, если ваша коллекция не может содержать нулевых элементов.

Я не сравнивал это с

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

Возможно, реализация языка C в Ruby Enumerable#selectтоже очень хороша.

Jvoorhis
источник
9

Альтернативное решение, которое будет работать в каждой реализации и работать за O (n) вместо O (2n) времени:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}
Педро Роло
источник
11
Вы имеете в виду, что он проходит по списку только один раз. Если следовать формальному определению, O (n) равно O (2n). Просто придирки :)
Дэниел Хеппер
1
@Daniel Harper :) Не только вы правы, но и в среднем случае, если пройтись по списку один раз, чтобы отбросить некоторые записи, а затем снова выполнить операцию, может быть фактически лучше в средних случаях :)
Педро Роло
Другими словами, вы делаете 2вещи , nраз вместо того , чтобы 1вещь nраз , а потом еще 1вещь nраз :) Одним из важных преимуществ inject/ в reduceтом , что он сохраняет все nilзначения в последовательности ввода , которая является более список-comprehensionly поведение
John La Роой
8

Я только что опубликовал в RubyGems гем comprehend , который позволяет вам делать это:

require 'comprehend'

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

Написано на C; массив проходит только один раз.

гистократ
источник
7

Enumerable имеет grepметод, первый аргумент которого может быть процедурой предиката, а второй необязательный аргумент - функцией сопоставления; так что работает следующее:

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

Это не так удобно для чтения, как пара других предложений (мне нравится gem от anoiaque simple select.mapили histocrat), но его сильные стороны в том, что он уже входит в стандартную библиотеку, является однопроходным и не требует создания временных промежуточных массивов. , и не требует значения, выходящего за пределы, как nilв compactпредложениях -using.

Питер Моулдер
источник
4

Это более кратко:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
анойак
источник
2
Или, для еще большего безупречного великолепия[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]

Это подходит для меня. Это тоже чисто. Да, то же самое map, но, думаю, collectделает код более понятным.


select(&:even?).map()

на самом деле выглядит лучше, увидев это ниже.

Винс
источник
2

Как упоминал Педро, вы можете объединить связанные вызовы с Enumerable#selectи Enumerable#map, избегая обхода выбранных элементов. Это правда, потому что Enumerable#selectэто специализация fold or inject. Я разместил поспешное введение в эту тему в сабреддите Ruby.

Слияние преобразований Array вручную может быть утомительным, поэтому, возможно, кто-то сможет поиграть с реализацией Роберта Гэмбла, comprehendчтобы сделать этот select/ mapшаблон красивее.

Jvoorhis
источник
2

Что-то вроде этого:

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?}
Guss
1

Другое решение, но, возможно, не лучшее

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

или

some_array.each_with_object([]) {|x, list| x % 2 == 0 ? list.push(x * 3) : nil }
Joegiralt
источник
0

Это один из способов приблизиться к этому:

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 

так что в основном мы преобразуем строку в правильный синтаксис ruby ​​для цикла, тогда мы можем использовать синтаксис 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]
Сэм Майкл
источник
0

Представлен Ruby 2.7, filter_mapкоторый в значительной степени обеспечивает то, что вы хотите (карта + компактность):

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

Вы можете прочитать об этом здесь .

Матеус Ричард
источник
0

https://rubygems.org/gems/ruby_list_comprehension

бесстыдный плагин для моего гема понимания списка Ruby, чтобы позволить идиоматическое понимание списка Ruby

$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]
Сэм Майкл
источник
-4

Я думаю, что наиболее подходящим для понимания списком будет следующий:

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

Поскольку Ruby позволяет нам помещать условное выражение после выражения, мы получаем синтаксис, аналогичный версии Python для понимания списка. Кроме того, поскольку selectметод не включает ничего, что приравнивается к false, все значения nil удаляются из результирующего списка, и не требуется вызова compact, как в случае, если бы мы использовали mapили collectвместо него.

Кристофер Роуч
источник
7
Похоже, это не работает. По крайней мере, в Ruby 1.8.6 [1,2,3,4,5,6] .select {| x | x * 3, если x% 2 == 0} оценивается как [2, 4, 6] Enumerable # select заботится только о том, оценивается ли блок как истинное или ложное, а не о том, какое значение он выводит, AFAIK.
Грег Кэмпбелл,