簡體   English   中英

Ruby 相當於 Python 的 dict 理解

[英]Ruby equivalent of Python's dict comprehension

我正在將 Python 項目重寫為 Ruby。

這是一個純粹的 Ruby 項目,所以沒有框架,例如附加到它的 Rails。

項目在整個地方都有很多字典理解。

例如:

original = {'one': 1, 'two': 2, 'three': 3}

squares = {name:value**2 for (name,value) in original.items()}
print(squares)

我在 Ruby 中得到的最接近的是:

original = { one: 1, two: 2, three: 3 }

squares = original.inject ({}) do | squared, (name,value) | 
  squared[name] = value ** 2;
  squared
end 
puts squares

這顯然有效,但我想知道在 Ruby 中是否有更方便或更易讀的方式來編寫它。

先感謝您。

讓我們退后幾步,暫時忽略 Ruby 和 Python 的細節。

數學集生成器符號

理解的概念最初來自數學集合構建符號,例如: E = { n ∈ ℕ| 2∣n }E定義為所有偶數自然數的集合, E = { 2n | n ∈ ℕ }

編程語言中的列表推導

直到 1969 年,這種 set-builder 符號在許多編程語言中激發了類似的構造,盡管直到 1970 年代,Phil Wadler 才為這些創造了術語理解 列表推導式最終在 1980 年代初期在 Miranda 中實現,這是一種極具影響力的編程語言。

然而,重要的是要理解這些推導不會向編程語言世界添加任何新的語義特征。 一般來說,沒有任何程序你可以寫出你不能沒有的理解。 Comprehensions 為表達這些類型的轉換提供了一種非常方便的語法,但它們沒有做任何標准遞歸模式(如foldmapscan展開和friends )無法實現的事情。

所以,讓我們先看看Python 的各種理解功能與標准遞歸模式的比較,然后看看這些遞歸模式如何在 Ruby 中可用。

Python

[注意:我將在這里使用 Python 列表推導語法,但這並不重要,因為列表、集合、字典推導和生成器表達式都工作相同。 我還將使用函數式編程語言的約定,將單字母變量用於集合元素,將復數用於集合,即x表示元素, xs表示“x-es 的集合”。]

以相同的方式轉換每個元素

[ f(x) for x in xs]

這使用轉換function 將原始集合的每個元素轉換為新集合的新元素。 這個新集合的元素數量與原始集合的數量相同,並且原始集合的元素與新集合的元素之間存在 1:1 的對應關系。

可以說原始集合的每個元素都映射到新集合的一個元素。 因此,這在許多編程語言中通常稱為map ,實際上,在 Python 中也稱為

map(f, xs)

相同,但嵌套

Python 允許您在單個理解中擁有多個for / in 這或多或少等同於嵌套映射,然后將其展平為單個集合:

[ f(x, y) for x in xs for y in ys]

這種映射然后展集合的組合通常稱為flatMap (應用於集合時)或綁定(應用於 Monads 時)

過濾

Python 操作支持的最后一個操作是過濾

[x for x in xs if p(x)]

這會將集合xs過濾成一個集合,該集合包含滿足謂詞p的原始元素的子集。 此操作通常稱為過濾器

隨意組合

顯然,您可以將所有這些結合起來,即您可以使用多個嵌套生成器進行理解,這些生成器過濾掉一些元素然后對其進行轉換。

Ruby

Ruby 還提供了上面提到的所有遞歸模式(或集合操作)等等。 在 Ruby 中,一個可以迭代的 object 稱為enumerable核心庫中的Enumerable mixin提供了很多有用且強大的收集操作。

Ruby 最初深受 Smalltalk 的啟發,Ruby 最初收集操作的一些舊名稱仍然是 go 回到了這個 Smalltalk 遺產。 在 Smalltalk collections 框架中,有一個關於所有 collections 方法彼此押韻的笑話,因此,Smalltalk 中的基本 collections 方法被稱為函數式編程中的基本 collections 方法[這里列出了它們的更多標准]:

  • collect: ,它將從塊返回的所有元素“收集”到一個新集合中,即這相當於map
  • select: ,它“選擇”所有滿足塊的元素,即這相當於filter
  • reject: ,它“拒絕”所有滿足塊的元素,即這與select:相反,因此等效於有時稱為filterNot的內容。
  • detect: ,它“檢測”滿足塊的元素是否在集合內,即這相當於contains 除了,它實際上也返回元素,所以它更像findFirst
  • inject:into: ... 漂亮的命名模式在某種程度上被打破了......:它確實將一個起始值“注入”到一個塊中,但這與它的實際作用有點緊張。 這相當於fold

因此,Ruby 擁有所有這些,甚至更多,它使用了一些原始命名,但幸運的是,它還提供了別名。

Map

在 Ruby 中, map最初被命名為Enumerable#collect但也可以作為Enumerable#map使用,這是大多數 Ruby 愛好者首選的名稱。

如上所述,這在 Python 中也可用,因為map內置 function。

平面圖

在 Ruby 中, flatMap最初命名為Enumerable#collect_concat ,但也可作為Enumerable#flat_map ,這是大多數 Ruby 愛好者首選的名稱。

篩選

在 Ruby 中,過濾器最初命名為Enumerable#select ,這是大多數 Ruby 愛好者首選的名稱,但也可用作Enumerable#find_all

篩選

在 Ruby 中, filterNot被命名為Enumerable#reject

查找優先

在 Ruby 中, findFirst最初命名為Enumerable#detect ,但也可用作Enumerable#find

折疊

在 Ruby 中, fold最初命名為Enumerable#inject ,但也可用作Enumerable#reduce

它也存在於 Python 作為functools.reduce

展開

在 Ruby 中,展開被命名為Enumerator::produce

掃描

遺憾的是 Ruby 中不提供掃描功能。 在 Python 作為itertools.accumulate可用。

深入研究遞歸模式

有了上面的命名法,我們現在知道你寫的東西叫做fold

squares = original.inject ({}) do |squared, (name, value)| 
  squared[name] = value ** 2
  squared
end 

你在這里寫的工作。 而我剛剛寫的那句話,居然深得驚人! 因為fold有一個非常強大的屬性:所有可以表示為迭代集合的東西都可以表示為 fold 換句話說,所有可以表示為在集合上遞歸的東西(用函數式語言),可以表示為循環/迭代集合的所有東西(用命令式語言),可以用任何上述方法表示的所有東西提到的函數( mapfilterfind ),可以使用 Python 的推導式表達的所有內容,可以使用我們尚未討論的一些附加函數(例如groupBy )表達的所有內容都可以使用fold表達。

如果你有fold ,你不需要任何東西! 如果您要從Enumerable中刪除除Enumerable#inject之外的所有方法,您仍然可以編寫之前可以編寫的所有內容; 您實際上可以僅使用Enumerable#inject重新實現您剛剛刪除的所有方法。 事實上,我曾經這樣做是為了好玩 您還可以實現上面提到的缺失掃描操作

fold確實是通用的並不一定很明顯,但可以這樣想:集合可以為空,也可以不為空。 fold有兩個 arguments,一個告訴它當列表為空時該做什么,一個告訴它當列表不為空時該做什么。 這些是僅有的兩種情況,因此所有可能的情況都會被處理。 因此, fold無所不能!

或者不同的觀點:集合是指令的 stream,可以是EMPTY指令,也可以是ELEMENT(value)指令。 fold是該指令集的骨架解釋器,作為程序員,您可以提供解釋這兩條指令的實現,即要折疊的兩個 arguments這些指令的解釋。 [我被介紹給這種令人大開眼界的對fold作為解釋器和將集合作為指令的解釋 stream 是由於Rúnar Bjarnason ,他 是 Scala 中的函數式編程的合著者Unison 編程語言的共同設計者。 不幸的是,我再也找不到原來的談話了,但重新審視的解釋器模式提出了一個更普遍的想法,也應該把它帶出來。]

請注意,您在這里使用fold的方式有些尷尬,因為您正在使用突變(即副作用)來進行深深植根於函數式編程的操作。 Fold使用一次迭代的返回值作為下一次迭代的起始值。 但是你正在做的操作是一個突變,它實際上並沒有為下一次迭代返回一個有用的值。 這就是為什么您必須返回剛剛修改的累加器。

如果您要使用Hash#merge以功能方式表達這一點,而無需突變,它看起來會更干凈:

squares = original.inject ({}) do |squared, (name, value)| 
  squared.merge({ name => value ** 2})
end 

但是,對於特定的用例,不是在每次迭代中返回一個的累加器並將其用於下一次迭代,您只想一遍又一遍地改變同一個累加器, Ruby提供了一個名為Enumerable#each_with_object的不同折疊變體Enumerable#each_with_object Enumerable#each_with_object完全忽略了塊的返回值,每次只傳遞相同的累加器 object。 令人困惑的是,塊中 arguments 的順序在Enumerable#inject (累加器第一,元素第二)和Enumerable#each_with_object (元素第一,累加器第二)之間顛倒:

squares = original.each_with_object ({}) do |(name, value), squared| 
  squared[name] = value ** 2}
end 

然而,事實證明,我們可以讓這更簡單。 我在上面解釋過fold是通用的,即它可以解決所有問題。 那么為什么我們首先要進行其他操作呢? 我們擁有它們的原因與我們擁有子例程、條件、異常和循環的原因相同,即使我們可以只使用GOTO來完成所有事情:表現力

如果您僅使用GOTO閱讀某些代碼,則必須“逆向工程” GOTO的每個特定用法的含義:它是否檢查條件,是否多次執行某些操作? 通過使用不同的、更專業的構造,您可以一眼就認出特定代碼的作用。

這同樣適用於這些收集操作。 例如,在您的情況下,您正在將原始集合的每個元素轉換為結果集合的新元素。 但是,您必須實際閱讀並理解該塊的作用,才能識別這一點。

然而,正如我們上面所討論的,有一個更專業的操作可以做到這一點: map 看到map 的每個人都會立即理解“哦,這是將每個元素 1:1 映射到新元素”,甚至不必看塊的作用。 因此,我們可以改為這樣編寫您的代碼:

squares = original.map do |name, value| 
  [name, value ** 2]
end.to_h

注意:Ruby 的集合操作大部分不是類型保留的,即轉換集合通常不會產生相同類型的集合。 相反,一般來說,收集操作大多返回Array s ,這就是為什么我們必須在最后調用Array#to_h

正如你所看到的,因為這個操作比fold更專業(它可以做任何事情),它讀起來更簡單,寫起來也更簡單(即塊的內部,你作為程序員必須寫的部分,比你上面的簡單)。

但實際上我們還沒有完成,事實證明,對於這種特殊情況,我們只想轉換Hash,實際上有一個更專業的操作可用: Hash#transform_values

squares = original.transform_values do |value| 
  value ** 2
end

結語

程序員最常做的一件事是迭代 collections 幾乎每一個用任何編程語言編寫的程序都以某種方式頌揚這一點。 因此,研究您的特定編程語言為此提供的操作非常有價值。

在 Ruby 中,這意味着研究Enumerable mixin以及ArrayHash提供的附加方法。

另外,研究Enumerator以及如何組合它們。

但研究這些操作從何而來的歷史也很有幫助,這實際上主要是函數式編程。 If you understand the history of those operations, you will actually be able to quickly familiarize yourself with collection operations in many languages, since they all borrow from that same history, eg ECMAScript, Python, .NET LINQ, Java Streams, C++ STL algorithms, and還有很多。

您可以通過這種方式在哈希上使用transform_values方法

original.transform_values { |v| v ** 2 }
 => {:one=>1, :two=>4, :three=>9} 

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM