在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#select
和Enumerable#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,因为如果我们已经使用map
或collect
那么就不需要调用compact。