在Ruby中列出理解

要做相当于Python列表parsing,我正在做以下事情:

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

有没有更好的方法来做到这一点…也许用一个方法调用?

如果你真的想,你可以像这样创build一个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 

我可能会按照你做的那样去做。

怎么样:

 some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact 

稍微干净一点,至less对我来说,根据一个快速的基准testing,比你的版本快大约15%…

我做了一个比较三个替代scheme和map-compact的快速基准testing,看起来确实是最好的select。

性能testing(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 

我与Rein Henrichs讨论了这个话题,他告诉我最好的解决scheme是

 map { ... }.compact` 

这是很有道理的,因为它避免了Enumerable#inject的不可变使用来构build中间数组,并且避免了增长导致分配的Array。 除了你的集合可以包含nil元素之外,它和其他的一样。

我没有比较这一点

 select {...}.map{...} 

Ruby的C实现Enumerable#select也可能是非常好的。

一个替代解决scheme,将在每个实现中运行,并在O(n)而不是O(2n)时间运行是:

 some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res} 

我刚刚发布了RubyGems的理解gem ,它可以让你这样做:

 require 'comprehend' some_array.comprehend{ |x| x * 3 if x % 2 == 0 } 

它用C写成; 数组只遍历一次。

在这个线程中,Ruby程序员似乎对列表理解有些困惑。 每一个响应都假设一个预先存在的数组进行转换。 但是列表理解的力量在于使用以下语法随时创build的数组:

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

以下是Ruby中的一个模拟(在这个线程中AFAIC中唯一的答案):

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

在上面的例子中,我创build了一个随机整数数组,但块可以包含任何东西。 但是,这将是一个Ruby列表理解。

Enumerable有一个grep方法,其第一个参数可以是谓词proc,其可选的第二个参数是映射函数; 所以下面的作品:

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

这不像其他一些build议(我喜欢anoiaque简单的select.map或者select.map的理解gem)那样可读,但是它的优点在于它已经是标准库的一部分,并且是单向的,并且不涉及创build临时的中间数组,并且不需要象在compact使用build议中使用的nil那样的越界值。

这更简洁:

 [1,2,3,4,5,6].select(&:even?).map{|x| x*3} 
 [1, 2, 3, 4, 5, 6].collect{|x| x * 3 if x % 2 == 0}.compact => [6, 12, 18] 

这对我行得通。 它也很干净。 是的,它和map是一样的,但是我认为collect使得代码更容易理解。


 select(&:even?).map() 

实际上看起来更好,看到下面。

就像佩德罗提到的,你可以将链式调用融合到Enumerable#selectEnumerable#map ,避免遍历选定的元素。 这是真实的,因为Enumerable#select是fold或inject一个特殊化。 我在Ruby subreddit上发表了一个简单的介绍 。

手动融合数组转换可能是单调乏味的,所以也许有人可以使用Robert Gamble的comprehend实现来使这个select / map模式更漂亮。

像这样的东西:

 def lazy(collection, &blk) collection.map{|x| blk.call(x)}.compact end 

叫它:

 lazy (1..6){|x| x * 3 if x.even?} 

哪个返回:

 => [6, 12, 18] 

我认为最常用的列表理解是:

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

由于Ruby允许我们在expression式之后放置条件语句,所以我们得到类似于列表理解的Python版本的语法。 另外,由于select方法不包含等于false任何内容,因此所有的nil值将从结果列表中删除,并且不需要调用compact,因为如果我们已经使用mapcollect那么就不需要调用compact。