Понимание списков в 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

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тоже очень хороша.


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шаблон красивее.


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 }

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 }

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



-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.
Грег Кэмпбелл,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.