Đọc hiểu danh sách trong Ruby


93

Để làm tương đương với việc hiểu danh sách Python, tôi đang làm như sau:

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

Có cách nào tốt hơn để làm điều này ... có lẽ với một cuộc gọi phương thức?


3
Cả câu trả lời của bạn và glenn mcdonald đều có vẻ ổn đối với tôi ... Tôi không hiểu bạn sẽ đạt được gì bằng cách cố gắng trở nên ngắn gọn hơn.
Pistos

1
giải pháp này chuyển danh sách hai lần. Không tiêm.
Pedro Rolo

2
Một số câu trả lời tuyệt vời ở đây nhưng cũng sẽ tuyệt vời khi xem các ý tưởng để hiểu danh sách trên nhiều bộ sưu tập.
Bo Jeanes

Câu trả lời:


55

Nếu bạn thực sự muốn, bạn có thể tạo một phương thức Array # domains như sau:

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

Bản in:

6
12
18

Tôi có lẽ sẽ chỉ làm theo cách bạn đã làm.


2
Bạn có thể sử dụng nhỏ gọn! để tối ưu hóa một chút
Alexey

9
Điều này thực sự không chính xác, hãy xem xét: [nil, nil, nil].comprehend {|x| x }cái nào trả về [].
Ted Kaplan

alexey, theo tài liệu, compact!trả về nil thay vì mảng khi không có mục nào được thay đổi, vì vậy tôi không nghĩ rằng điều đó hoạt động.
Binary Phile

89

Cai đo thê nao:

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

Sạch hơn một chút, ít nhất là theo sở thích của tôi và theo một bài kiểm tra điểm chuẩn nhanh hơn khoảng 15% so với phiên bản của bạn ...


4
cũng như some_array.map{|x| x * 3 unless x % 2}.compact, được cho là dễ đọc hơn / ruby-esque.
nightpool

5
@nightpool unless x%2không có hiệu lực vì 0 là true trong ruby. Xem: gist.github.com/jfarmer/2647362
Abhinav Srivastava

30

Tôi đã thực hiện một điểm chuẩn nhanh so sánh ba lựa chọn thay thế và map-compact thực sự có vẻ là lựa chọn tốt nhất.

Kiểm tra hiệu suất (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

Các kết quả

/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
Cũng sẽ rất thú vị khi xem reduceđiểm chuẩn này (xem stackoverflow.com/a/17703276 ).
Adam Lindberg

3
inject==reduce
ben.snape

map_compact có thể nhanh hơn nhưng nó đang tạo một mảng mới. tiêm là không gian hiệu quả hơn map.compact và select.map
bibstha

11

Dường như có một số nhầm lẫn giữa các lập trình viên Ruby trong chủ đề này liên quan đến khả năng hiểu danh sách là gì. Mỗi phản hồi đơn giả sử một số mảng có sẵn để biến đổi. Nhưng sức mạnh của khả năng hiểu danh sách nằm trong một mảng được tạo ngay lập tức với cú pháp sau:

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

Sau đây sẽ là một tương tự trong Ruby (câu trả lời thích hợp duy nhất trong chuỗi này, AFAIC):

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

Trong trường hợp trên, tôi đang tạo một mảng các số nguyên ngẫu nhiên, nhưng khối có thể chứa bất kỳ thứ gì. Nhưng đây sẽ là một cách hiểu danh sách Ruby.


1
Làm thế nào bạn sẽ làm những gì OP đang cố gắng làm?
Andrew Grimm

2
Trên thực tế, tôi thấy bây giờ OP đã có một số danh sách hiện có mà tác giả muốn chuyển đổi. Nhưng khái niệm nguyên mẫu về khả năng hiểu danh sách liên quan đến việc tạo một mảng / danh sách mà trước đó không tồn tại bằng cách tham chiếu đến một số lần lặp. Nhưng trên thực tế, một số định nghĩa chính thức nói rằng việc hiểu danh sách hoàn toàn không thể sử dụng bản đồ, vì vậy ngay cả phiên bản của tôi cũng không phải là kosher - nhưng tôi đoán gần như người ta có thể nhận được trong Ruby.
Đánh dấu

5
Tôi không hiểu làm thế nào ví dụ Ruby của bạn được cho là một chất tương tự của ví dụ Python của bạn. Mã Ruby nên đọc: square = (0..9) .map {| x | x ** 2}
michau

4
Mặc dù @michau đúng, toàn bộ điểm của việc hiểu danh sách (mà Mark đã bỏ qua) là bản thân việc hiểu danh sách không sử dụng không tạo mảng - nó sử dụng trình tạo và đồng quy trình để thực hiện tất cả các phép tính theo cách trực tuyến mà không phân bổ bộ nhớ nào cả (ngoại trừ biến tạm thời) cho đến khi (iff) kết quả nằm trong một biến mảng - đây là mục đích của dấu ngoặc vuông trong ví dụ python, để thu gọn khả năng hiểu thành một tập kết quả. Ruby không có cơ sở tương tự như máy phát điện.
Guss

4
Ồ vâng, nó có (kể từ Ruby 2.0): square_of_all_natural_numbers = (0..Float :: INFINITY) .lazy.map {| x | x ** 2}; p squares_of_all_natural_numbers.take (10) .to_a
michau

11

Tôi đã thảo luận về chủ đề này với Rein Henrichs, người nói với tôi rằng giải pháp hiệu quả nhất là

map { ... }.compact

Điều này có ý nghĩa tốt vì nó tránh xây dựng Mảng trung gian như với cách sử dụng không thay đổi Enumerable#injectvà nó tránh phát triển Mảng gây ra phân bổ. Nó chung chung như bất kỳ bộ sưu tập nào khác trừ khi bộ sưu tập của bạn có thể chứa các phần tử nil.

Tôi chưa so sánh điều này với

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

Có thể là việc triển khai C của Ruby Enumerable#selectcũng rất tốt.


9

Một giải pháp thay thế sẽ hoạt động trong mọi lần triển khai và chạy trong thời gian O (n) thay vì O (2n) là:

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

11
Bạn có nghĩa là nó chỉ duyệt qua danh sách một lần. Nếu bạn đi theo định nghĩa chính thức, O (n) bằng O (2n). Just nitpicking :)
Daniel Hepper

1
@Daniel Harper :) Không chỉ bạn đúng mà còn đối với trường hợp trung bình, việc chuyển đổi danh sách một lần để loại bỏ một số mục nhập và sau đó lại thực hiện một thao tác có thể tốt hơn về mặt thực tế trong các trường hợp trung bình :)
Pedro Rolo

Nói cách khác, bạn đang làm 2việc nlần thay vì 1điều nlần và sau đó một 1điều nlần :) Một lợi thế quan trọng của inject/ reducelà nó bảo bất kỳ nilgiá trị trong chuỗi đầu vào mà là nhiều hành vi list-comprehensionly
John La Rooy

8

Tôi vừa xuất bản viên ngọc hiểu biết cho RubyGems, cho phép bạn làm điều này:

require 'comprehend'

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

Nó được viết bằng C; mảng chỉ được duyệt một lần.


7

Enumerable có một grepphương thức có đối số đầu tiên có thể là một proc vị từ và đối số thứ hai tùy chọn của nó là một hàm ánh xạ; vì vậy các hoạt động sau đây:

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

Điều này không thể đọc được như một vài gợi ý khác (tôi thích select.mapđá quý đơn giản hoặc histocrat của anoiaque), nhưng điểm mạnh của nó là nó đã là một phần của thư viện tiêu chuẩn, và là một lần và không liên quan đến việc tạo các mảng trung gian tạm thời và không yêu cầu giá trị nằm ngoài giới hạn như nilđược sử dụng trong các compactđề xuất sử dụng.


4

Điều này ngắn gọn hơn:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}

2
Hoặc, thậm chí nhiều điểm miễn khiếp sợ[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Jörg W Mittag

4
[1, 2, 3, 4, 5, 6].collect{|x| x * 3 if x % 2 == 0}.compact
=> [6, 12, 18]

Nó ổn với tôi. Nó cũng sạch sẽ. Có, nó giống như map, nhưng tôi nghĩ collectlàm cho mã dễ hiểu hơn.


select(&:even?).map()

thực sự trông đẹp hơn, sau khi nhìn thấy nó bên dưới.


2

Giống như Pedro đã đề cập, bạn có thể hợp nhất với nhau các cuộc gọi bị xích vào Enumerable#selectEnumerable#map, tránh một traversal trên các yếu tố lựa chọn. Điều này đúng vì Enumerable#selectlà một chuyên môn hóa của nếp gấp hoặc inject. Tôi đã đăng một đoạn giới thiệu vội vàng về chủ đề này tại Ruby subreddit.

Việc kết hợp thủ công các phép biến đổi Mảng có thể khá tẻ nhạt, vì vậy có thể ai đó có thể chơi với cách comprehendtriển khai của Robert Gamble để làm cho mẫu select/ này mapđẹp hơn.


2

Một cái gì đó như thế này:

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

Gọi nó đi:

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

Trả về:

=> [6, 12, 18]

Có gì sai với việc xác định lazytrên Mảng và sau đó:(1..6).lazy{|x|x*3 if x.even?}
Guss

1

Một giải pháp khác nhưng có lẽ không phải là giải pháp tốt nhất

some_array.flat_map {|x| x % 2 == 0 ? [x * 3] : [] }

hoặc là

some_array.each_with_object([]) {|x, list| x % 2 == 0 ? list.push(x * 3) : nil }

0

Đây là một cách để tiếp cận điều này:

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 

vì vậy về cơ bản chúng ta đang chuyển đổi một chuỗi thành cú pháp ruby ​​thích hợp cho vòng lặp, sau đó chúng ta có thể sử dụng cú pháp python trong một chuỗi để thực hiện:

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]

hoặc nếu bạn không thích giao diện của chuỗi hoặc phải sử dụng lambda, chúng tôi có thể bỏ qua nỗ lực sao chép cú pháp python và làm điều gì đó như sau:

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

0

Ruby 2.7 đã giới thiệu filter_mapcái nào đạt được khá nhiều thứ bạn muốn (bản đồ + nhỏ gọn):

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

Bạn có thể đọc thêm về nó ở đây .



-4

Tôi nghĩ điều dễ hiểu nhất trong danh sách sẽ là:

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

Vì Ruby cho phép chúng ta đặt điều kiện sau biểu thức, chúng ta nhận được cú pháp tương tự như phiên bản Python của khả năng hiểu danh sách. Ngoài ra, vì selectphương thức không bao gồm bất kỳ thứ gì tương đương với false, nên tất cả các giá trị nil sẽ bị xóa khỏi danh sách kết quả và không cần gọi đến compact như trường hợp chúng ta đã sử dụng maphoặc collectthay thế.


7
Điều này dường như không hoạt động. Ít nhất trong Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 nếu x% 2 == 0} cho giá trị là [2, 4, 6] Enumerable # select chỉ quan tâm đến việc khối đó đánh giá là true hay false, chứ không phải giá trị mà nó xuất ra, AFAIK.
Greg Campbell
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.