Python的dict理解的Ruby等价物

tvz2xvvm  于 2023-06-29  发布在  Ruby
关注(0)|答案(2)|浏览(99)

我正在将一个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来编写它。

ncgqoxb0

ncgqoxb01#

你可以用这种方式在哈希上使用transform_values方法

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

gupuwyp22#

让我们后退几步,暂时忽略Ruby和Python的细节。

数学集合生成器表示法

  • comprehension* 的概念最初来自mathematical set-builder notation,例如:比如这样:E = { n ∈| 2 n },它定义E为所有偶数自然数的集合,E = { 2n| n ∈}

编程语言中的列表解析

这种集合构建器表示法启发了许多编程语言中的类似结构,一直追溯到1969年,尽管直到20世纪70年代Phil Wadler才为这些语言创造了术语 * comprehension *。List comprehensions最终在20世纪80年代初在米兰达中实现,这是一种非常有影响力的编程语言。
然而,重要的是要明白,这些理解 * 并没有 * 为编程语言的世界添加任何新的语义特征。一般来说,没有一个程序你可以写一个理解,你也不能写没有。解析提供了一种非常方便的语法来表达这些类型的转换,但是它们没有做任何标准的递归模式无法实现的事情,比如foldmapscanunfold和朋友。
因此,让我们首先看看Python的解析的各种特性与标准递归模式的比较,然后看看这些递归模式在Ruby中是如何可用的。

Python

[Note我将在这里使用Python列表解析语法,但这并不重要,因为列表、集合、字典解析和生成器表达式都是一样的。我还将使用函数式编程中的约定,对集合元素使用单字母变量,对集合使用复数,即x表示一个元素,xs表示“一个x-es的集合”。]

对每个元素进行相同的转换

[f(x) for x in xs]

这将使用转换函数将原始集合的每个元素转换为新集合的新元素。该新集合具有与原始集合相同数量的元素,并且在原始集合的元素与新集合的元素之间存在1:1的对应关系。
可以说,原始集合的每个元素都被Map到新集合的一个元素。因此,这在许多编程语言中通常被称为 map,实际上,在Python中也被称为:

map(f, xs)

相同,但嵌套

Python允许你在一个解析中有多个for/in。这或多或少相当于拥有 * 嵌套 * Map,然后将其 * 扁平化 * 为单个集合:

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

这种 mapping 然后 * flatten * 集合的组合通常称为 flatMap(当应用于集合时)或 bind(当应用于Monad时)。

过滤

Python解析支持的最后一个操作是 filtering

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

这将把集合xs过滤成一个集合,该集合包含满足 predicate p的原始元素的子集。这种操作通常称为 filter

随意合并

显然,您可以将所有这些结合起来,即。你可以有一个包含多个嵌套生成器的解析,这些生成器过滤掉一些元素,然后转换它们。

Ruby

Ruby还提供了上面提到的所有 * 递归模式 *(或 * 集合操作 *),以及更多。在Ruby中,可以迭代的对象被称为 enumerableEnumerable mixin in the core library提供了许多有用且强大的集合操作。
Ruby最初深受Smalltalk的启发,Ruby最初的集合操作的一些旧名称仍然可以追溯到Smalltalk的遗产。在Smalltalk collections框架中,有一个关于所有collections方法彼此押韵的笑话,因此,Smalltalk中的基本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内置函数使用。

FlatMap

在Ruby中,flatMap 最初名为Enumerable#collect_concat,但也可以作为Enumerable#flat_map使用,这是大多数Ruby爱好者喜欢的名称。

过滤器

在Ruby中,filter 最初被命名为Enumerable#select,这是大多数Ruby爱好者喜欢的名称,但也可以作为Enumerable#find_all使用。

FilterNot

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

FindFirst

在Ruby中,findFirst 最初名为Enumerable#detect,但也可以作为Enumerable#find使用。

折叠

在Ruby中,fold 最初名为Enumerable#inject,但也可以作为Enumerable#reduce使用。
它在Python中也以functools.reduce的形式存在。

展开

在Ruby中,unfold 被命名为Enumerator::produce

扫描

深入研究递归模式

根据上面的命名法,我们现在知道你写的东西被称为 fold

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

你写的东西在这里工作。而我刚才写的那句话,竟然出奇的深刻!因为 fold 有一个非常强大的属性:所有可以表示为遍历集合的东西都可以表示为折叠。换句话说,可以表示为在集合上递归的所有内容(在函数式语言中),可以表示为循环/迭代集合的所有内容(在命令式语言中),可以用上述任何一种功能表达的一切(mapfilterfind),所有可以用Python的解析式表达的内容,所有可以使用我们尚未讨论的一些附加函数来表示的内容(例如:groupBy)可以使用 fold 来表示。
如果你有 fold,你就不需要其他任何东西了!如果您要从Enumerable中删除除Enumerable#inject之外的所有方法,您仍然可以编写以前可以编写的所有方法;实际上,你可以使用Enumerable#inject重新实现你刚刚删除的所有方法。事实上,I did that once for fun as an exercise。也可以是implement the missing scan operation mentioned above

  • fold* 是否真的是通用的并不一定是显而易见的,但可以这样想:集合可以是空的也可以不是。fold 有两个参数,一个告诉它当集合为空时该做什么,另一个告诉它当集合不为空时该做什么。只有这两种情况,所以所有可能的情况都得到了处理。因此,fold 可以做任何事情!

另一种观点:集合是指令流,或者是EMPTY指令,或者是ELEMENT(value)指令。fold 是该指令集的 backbone * 解释器 *,作为程序员,您可以为这两条指令的解释提供实现,即 *fold**的两个参数是 * 这些指令的解释。[我被介绍到这个令人大开眼界的解释 fold 作为解释器和集合作为指令流是由于Rúnar BjarnasonFunctional Programming in Scala的合著者和Unison Programming Language的共同设计者。不幸的是,我再也找不到原来的演讲了,但是The Interpreter Pattern Revisited提出了一个更一般的想法,也应该把它带过来。
请注意,这里使用 fold 的方式有些尴尬,因为您使用的是 mutation(即副作用)的操作,该操作深深植根于函数式编程。Fold 使用一次迭代的返回值作为下一次迭代的起始值。但是你正在做的操作是一个 mutation,它实际上不会为下一次迭代返回有用的值。这就是为什么你必须返回你刚刚修改的累加器。
如果你使用Hash#merge以函数的方式来表达它,没有突变,它看起来会更清晰:

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

然而,对于 specific 用例,而不是在每次迭代中返回 * new* 累加器并在下一次迭代中使用它,你想一遍又一遍地 mutate 相同的累加器,Ruby提供了一个名为Enumerable#each_with_objectfold 的不同变体,它完全 * 忽略 * 块的返回值,每次只传递相同的累加器对象。令人困惑的是,块中参数的顺序在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 的人都会立即理解“哦,这是将每个元素1:1Map到一个新元素”,甚至不必看块做了什么。因此,我们可以这样编写代码:

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

结语

程序员最常做的事情之一是 * 迭代集合 。实际上,用任何编程语言编写的每一个程序都以某种形式做到这一点。因此,研究特定编程语言为此提供的操作是非常有价值的。
在Ruby中,这意味着 * 学习Enumerable mixin
以及ArrayHash提供的其他方法。
另外,学习Enumerator s以及如何合并它们。
但是研究这些操作的来源也是非常有帮助的,这主要是函数式编程。如果你了解这些操作的历史,你将能够很快熟悉许多语言中的集合操作,因为它们都借鉴了相同的历史,例如。ECMAScript、Python、.NET LINQ、Java StreamsC++ STL algorithmsSwift等等。

相关问题