Розуміння списку в Ruby


93

Щоб зробити еквівалент розуміння списку Python, я роблю наступне:

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

Чи є кращий спосіб зробити це ... можливо, за допомогою одного виклику методу?


3
Як ваші, так і відповіді Глена Макдональда здаються мені чудовими ... Я не бачу, що ви отримаєте, намагаючись бути більш стислими, ніж будь-який із них.
Пістос,

1
це рішення перетинає список два рази. Ін'єкція ні.
Педро Роло,

2
Тут є кілька чудових відповідей, але це також було б чудово, і тут можна побачити ідеї для розуміння списків у багатьох колекціях.
Bo Jeanes,

Відповіді:


55

Якщо ви дійсно хочете, ви можете створити метод 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

Я, мабуть, просто зробив би це так, як ти це зробив.


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, що, можливо, є більш читабельним / рубіновим.
нічний пул

5
@nightpool unless x%2не має ефекту, оскільки 0 в рубіні неправдиве. Див .: gist.github.com/jfarmer/2647362
Шрівастава

30

Я зробив швидкий орієнтир, порівнюючи три альтернативи, і компактна карта справді здається найкращим варіантом.

Тест продуктивності (рейки)

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 є економічно вищим, ніж map.compact і select.map
bibstha

11

У цій темі серед програмістів Ruby існує певна плутанина щодо розуміння списку. Кожна відповідь передбачає перетворення певного існуючого масиву. Але сила розуміння списку полягає в масиві, створеному на льоту з таким синтаксисом:

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

Наступне буде аналогом Ruby (єдина відповідна відповідь у цій темі, AFAIC):

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

У наведеному вище випадку я створюю масив випадкових цілих чисел, але блок може містити що завгодно. Але це було б розумінням списку Рубі.


1
Як би ви зробили те, що намагається зробити ОП?
Andrew Grimm,

2
Насправді я бачу, що зараз у самого OP був якийсь існуючий список, який автор хотів перетворити. Але архетипна концепція розуміння списку передбачає створення масиву / списку там, де його раніше не існувало, посиланням на якусь ітерацію. Але насправді, деякі офіційні визначення говорять, що для розуміння списку взагалі не можна використовувати карту, тому навіть моя версія не кошерна - але, напевно, настільки близька, наскільки це можна було отримати в Ruby.
Марк

5
Я не розумію, як ваш приклад Ruby повинен бути аналогом вашого прикладу Python. Код Ruby повинен читати: квадрати = (0..9) .map {| x | x ** 2}
мхау

4
Незважаючи на те, що @michau має рацію, вся суть розуміння списку (яким Марк знехтував) полягає в тому, що саме розуміння списку не використовує не генерує масиви - воно використовує генератори та спільні підпрограми, щоб робити всі обчислення в потоковому режимі, взагалі не виділяючи місця для зберігання (крім temp variables) до (iff) результатів, що потрапляють у змінну масиву - це мета квадратних дужок у прикладі 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{...}

Цілком можливо, що реалізація Ruby C 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значення у вхідній послідовності, що є більшою поведінкою для розуміння списку
Джон Ла Рой,


7

Enumerable має grepметод, перший аргумент якого може бути предикатом proc, а необов’язковий другий аргумент - функція відображення; отже, працює наступне:

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

Це не так читабельно, як пара інших пропозицій (мені подобається простий select.mapсамоцвіт anoiaque або розуміння гістократа), але його сильні сторони полягають у тому, що він вже є частиною стандартної бібліотеки, є однопрохідним і не передбачає створення тимчасових проміжних масивів , і не вимагає значень, що виходять за межі, як це було nilвикористано в compactпропозиціях -use.


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(:*))
Йорг Ш Міттаг

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є спеціалізацією складання або inject. Я опублікував поспішне вступ до цієї теми в subreddit 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 

тому в основному ми перетворюємо рядок у правильний рубіновий синтаксис циклу, тоді ми можемо використовувати синтаксис 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 видаляються із результуючого списку, і жоден виклик компактування не потрібен, як це було б у випадку, якщо б ми використовували mapабо collectзамість цього.


7
Здається, це не працює. Принаймні в Ruby 1.8.6, [1,2,3,4,5,6] .виберіть {| x | x * 3, якщо x% 2 == 0} обчислюється на [2, 4, 6] Enumerable # select дбає лише про те, чи оцінює блок значення true чи false, а не про те, яке значення він виводить, AFAIK.
Грег Кемпбелл,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.