Як зробити в Ruby хеш з масиву?


76

У мене є простий масив:

arr = ["apples", "bananas", "coconuts", "watermelons"]

У мене також є функція, fяка виконує операцію над одним рядковим введенням і повертає значення. Ця операція дуже дорога, тому я хотів би запам'ятати результати в хеші.

Я знаю, що можу зробити бажаний хеш приблизно таким чином:

h = {}
arr.each { |a| h[a] = f(a) }

Що я хотів би зробити, це не потрібно ініціалізувати h, щоб я міг написати щось подібне:

h = arr.(???) { |a| a => f(a) }

Чи можна це зробити?

Відповіді:


128

Скажімо, у вас є функція з цікавою назвою: "f"

def f(fruit)
   fruit + "!"
end

arr = ["apples", "bananas", "coconuts", "watermelons"]
h = Hash[ *arr.collect { |v| [ v, f(v) ] }.flatten ]

дасть вам:

{"watermelons"=>"watermelons!", "bananas"=>"bananas!", "apples"=>"apples!", "coconuts"=>"coconuts!"}

Оновлено:

Як зазначалося в коментарях, Ruby 1.8.7 вводить для цього приємніший синтаксис:

h = Hash[arr.collect { |v| [v, f(v)] }]

Думаю, ви мали на увазі ... { |v| [v, f(v)] }, але це зробило трюк!
Wizzlewott

3
Тільки одне - чому там *поруч *arr.collect?
Джеріко

3
@Jeriko - оператор splat *збирає список у масив або розгортає масив у список, залежно від контексту. Тут він розгортає масив у списку (для використання в якості елементів для нового хешу).
Телемах

2
Після перегляду відповіді Йорга і думати над цим ще, помітить , що ви можете видалити як *і flattenдля більш простої версії: h = Hash[ arr.collect { |v| [ v, f(v) ] } ]. Але я не впевнений, чи є щось, що я не бачу.
Телемах

3
У Ruby 1.8.7 потворне Hash[*key_pairs.flatten]просто Hash[key_pairs]. Набагато приємніше, і require 'backports'якщо ви ще не оновили версію 1.8.6.
Marc-André Lafortune

56

Зробив кілька швидких, брудних тестів щодо деяких з поданих відповідей. (Ці висновки можуть бути не зовсім ідентичними вашим на основі версії Ruby, дивного кешування тощо, але загальні результати будуть схожими.)

arr є колекцією об'єктів ActiveRecord.

Benchmark.measure {
    100000.times {
        Hash[arr.map{ |a| [a.id, a] }]
    }
}

Бенчмарк @ real = 0.860651, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.0, @ utime = 0.8500000000000005, @ total = 0.8500000000000005

Benchmark.measure { 
    100000.times {
        h = Hash[arr.collect { |v| [v.id, v] }]
    }
}

Бенчмарк @ real = 0.74612, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.010000000000000009, @ utime = 0.740000000000002, @ total = 0.750000000000002

Benchmark.measure {
    100000.times {
        hash = {}
        arr.each { |a| hash[a.id] = a }
    }
}

Бенчмарк @ real = 0.627355, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.010000000000000009, @ utime = 0.6199999999999974, @ total = 0.6299999999999975

Benchmark.measure {
    100000.times {
        arr.each_with_object({}) { |v, h| h[v.id] = v }
    }
}

Бенчмарк @ real = 1.650568, @ cstime = 0.0, @ cutime = 0.0, @ stime = 0.12999999999999998, @ utime = 1.51, @ total = 1.64

На закінчення

Те, що Ruby виразний і динамічний, не означає, що вам слід завжди вибирати найкрасивіше рішення. Основна кожна петля була найшвидшою у створенні хешу.


7
Ти, друже, чудовий, що робиш домашнє завдання та публікуєш його :)
Олександр Берд

Трохи швидше використовувати змінну циклу, збільшену вручну: у мене немає вашого набору даних - я щойно приготував тривіальний об’єкт з доступом @id і більш-менш узгодив ваші номери, але пряма ітерація збрила пару%. Стилістично я віддаю перевагу {} .tap {| h | ....} до присвоєння хешу, тому що мені подобаються інкапсульовані шматки.
android.weasel



11

Це те, що я, мабуть, написав би:

h = Hash[arr.zip(arr.map(&method(:f)))]

Просто, зрозуміло, очевидно, декларативно. Що ще ти можеш хотіти?


1
Мені подобається zipтак само, як і наступному хлопцеві, але оскільки ми вже телефонуємо map, чому б не залишити це зараз? h = Hash[ arr.map { |v| [ v, f(v) ] } ]Чи є у вашої версії перевага, яку я не бачу?
Телемах

@ Telemachus: З усім кодом Haskell, який я читав, я щойно звик до безточкового програмування, ось і все.
Jörg W Mittag

5

Я роблю це, як описано в цій чудовій статті http://robots.thoughtbot.com/iteration-as-an-anti-pattern#build-a-hash-from-an-array

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h.merge(fruit => f(fruit)) }

Більше інформації про injectметод: http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-inject


Це робить mergeдля кожного кроку ітерації. Злиття - O (n), як і ітерація. Таким чином, це, O(n^2)хоча сама проблема очевидно лінійна. В абсолютному вираженні я просто спробував це на масиві з елементами 100 тис., І це зайняло 730 seconds, тоді як інші методи, згадані в цьому потоці, займали десь від 0.7до 1.1 seconds. Так, це уповільнення на факторі 700 !
Маттіас Вінкельманн,

1

Ще одне, трохи чіткіше ІМХО -

Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]

Використовуючи довжину як f () -

2.1.5 :026 > array = ["apples", "bananas", "coconuts", "watermelons"]
 => ["apples", "bananas", "coconuts", "watermelons"] 
2.1.5 :027 > Hash[*array.reduce([]) { |memo, fruit| memo << fruit << fruit.length }]
 => {"apples"=>6, "bananas"=>7, "coconuts"=>8, "watermelons"=>11} 
2.1.5 :028 >

1

на додаток до відповіді Владо Цінгеля (я ще не можу додати коментар, тому я додав відповідь).

Впорскування також можна використовувати таким чином: блок повинен повернути акумулятор. Тільки призначення у блоці повертає значення призначення, і повідомляється про помилку.

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h[fruit]= f(fruit); h }

Я порівняв дві версії: використання злиття подвоює час виконання. Вищевказана версія для ін’єкцій є порівнянням з
колекційною
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.