Ruby中的列表理解


93

为了进行等效于Python列表推导的操作,我正在执行以下操作:

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

是否有更好的方法来执行此操作……也许只用一个方法调用?


3
您和glenn mcdonald的答案对我来说似乎都很好。。。
Pistos

1
此解决方案横穿了列表两次。注入没有。
Pedro Rolo

2
这里有一些很棒的答案,但是也可以看到跨多个集合的列表理解的想法。
Bo Jeanes 2012年

Answers:


55

如果确实需要,可以创建一个像这样的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

我可能会按照您的方式来做。


2
您可以使用紧凑型!优化一点
Alexey

9
考虑一下[nil, nil, nil].comprehend {|x| x },这实际上是不正确的: 返回[]
2013年

根据文档,alexey compact!在没有任何更改的情况下返回nil而不是数组,因此我认为这不起作用。
Binary Phile 2014年

89

怎么样:

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

稍微干净一点,至少符合我的口味,根据快速基准测试,比您的版本快15%...


4
以及some_array.map{|x| x * 3 unless x % 2}.compact,可以说更具可读性/红宝石风格。
2015年

5
@nightpool unless x%2无效,因为0在ruby中是正确的。参见:gist.github.com/jfarmer/2647362
Abhinav Srivastava

30

我做了一个快速基准测试,比较了这三种选择,而map-compact似乎确实是最好的选择。

性能测试(路轨)

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可能更快,但它正在创建一个新数组。注入比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)} 

在上述情况下,我正在创建一个随机整数数组,但是该块可以包含任何内容。但这将是一个Ruby列表理解。


1
您将如何执行OP的尝试?
安德鲁·格林

2
实际上,我现在看到OP本身已有一些作者想要转换的列表。但是列表理解的原型概念涉及通过引用一些迭代来创建以前不存在的数组/列表。但是实际上,一些正式的定义说列表理解根本不能使用map,所以即使我的版本也不是kosher,但我猜它在Ruby中可以达到的程度。
2012年

5
我不知道您的Ruby示例应该如何与您的Python示例类似。Ruby代码应显示为:squares =(0..9).map {| x | x ** 2}
michau 2012年

4
虽然@michau是正确的,但列表理解的全部要点(马克忽略了)是列表理解本身不使用不生成数组-它使用生成器和协例程以流方式进行所有计算而根本不分配存储空间(除了临时变量)直到(iff)结果放入数组变量中-这是python示例中方括号的目的,用于将理解范围合并为一组结果。Ruby没有类似于生成器的功能。
2014年

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 2014年

11

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

map { ... }.compact

这很有道理,因为它避免了像不变使用那样构建中间Enumerable#injectArray,并且避免了增大Array从而导致分配的问题。除非您的集合可以包含nil个元素,否则它与其他任何一个元素一样通用。

我没有将此与

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 :)不仅您是正确的,而且对于一般情况也是如此,遍历该列表一次以丢弃某些条目,然后再次执行一次操作在一般情况下可能会更好:)
Pedro Rolo

换句话说,你正在做2的事情n的时间,而不是1事情n次,然后另一1件事n倍:)的一个重要优点inject/ reduce是,它保留任何nil到底哪个更列表comprehensionly行为的输入序列值
约翰·拉ROOY

8

我刚刚将理解的gem发布到了RubyGems,这使您可以执行此操作:

require 'comprehend'

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

用C写的;该数组仅被遍历一次。


7

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

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

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



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#selectEnumerable#map,避免过度所选元素遍历。这是正确的,因为Enumerable#select是fold或的特殊化inject。我在Ruby subreddit上草率介绍了该主题。

手动融合Array转换可能很乏味,因此也许有人可以使用Robert Gamble的comprehend实现来使此select/ map模式更漂亮。



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 

因此,基本上,我们将字符串转换为正确的ruby语法进行循环,然后可以在字符串中使用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]

或者,如果您不喜欢字符串的外观或必须使用lambda,我们可以放弃尝试镜像python语法并尝试执行以下操作:

S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]



-4

我认为最能理解列表的内容如下:

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

由于Ruby允许我们将条件放在表达式之后,因此获得的语法类似于列表理解的Python版本。另外,由于该select方法不包含任何等于的false值,因此将从结果列表中删除所有nil值,并且不需要调用compact,就像我们曾经使用mapcollect代替的情况一样。


7
这似乎不起作用。至少在Ruby 1.8.6中,[1,2,3,4,5,6] .select {| x | x * 3,如果x%2 == 0}求值为[2,4,6] Enumerable#select只关心该块的求值是true还是false,而不是它输出的值是AFAIK。
格雷格·坎贝尔
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.