简体   繁体   中英

What is the equivalent of Python's itertools.product in Ruby?

I'm looking for a method that has the same effect as Python's itertools.product in Ruby. Take the following Python code:

from itertools import product

chars = []
for i in range(97,123):
    chars.append(chr(i))

for a in range(1,3):
    for i in product(chars,repeat=a):
        s = ''.join(i)
        print s

That outputs something like this:

a, b, c... x, y, z, aa, ab, ac... ax, ay, az, ba, bb, bc.. etc.

I tried to translate that into Ruby:

(1..2).each do |n|
  ('a'..'z').to_a.combination(n).each do |c|
    s = c.join
    puts s
  end
end

But the output isn't the same. The one-character ones work fine (az), but when it goes into two-character ones, it doesn't work as I expected:

ab, ac, ad.. ax, ay, az, bc, bd, be

It isn't generating aa , ba or bb - so it appears it's generating all the combinations without repeating characters or something?

So what method should I use to generate all the combinations like itertools.product does in Python?

您拥有类似于itertools.product的 Array#product

I would write (simplified for 3 elements, Ruby 1.9 required):

xs = ["a", "b", "c"]
strings = 1.upto(xs.size).flat_map do |n| 
  xs.repeated_permutation(n).map(&:join)
end
#=> ["a", "b", "c", "aa", "ab", "ac", ...,  "cca", "ccb", "ccc"]

A lazy solution: you could easily write it with each s instead of map s, but let's check "lazy" from Ruby 2.0:

xs = ("a".."z").to_a
strings = 1.upto(xs.size).lazy.flat_map do |n| 
  xs.repeated_permutation(n).lazy.map(&:join)
end

Magic (not very pretty though):

a = ('a'..'z').to_a
result = (0..2).map { |n| 
  a.product(*n.times.inject([]) { |s,x| s << a }) }.map { |x| x.map(&:join) } 
}

puts result

Explanation: in order to work as the python product you need to repeat the array n-1 times in the product arguments.

So product('abc', repeat=n) is the same in ruby as:

a = ['a','b','c']
a.product()     # n = 1
a.product(a)    # n = 2
a.product(a, a) # n = 3

That's what the nasty inject does in the above code. It builds such an "argument array" automatically. It's not very efficient code though, so don't try to build large "products" with it.

After I wrote this, I noticed Casper's solution which is essentially the same. Some might find this one more readable so I am leaving it..

arr = ['a', 'b', 'c']

p (0..2).inject([]) { |acc, a|
  acc + arr.product(*[arr]*a).map(&:join)
}

=> ["a", "b", "c", "aa", "ab", "ac", "ba", "bb", "bc", "ca", "cb", "cc", "aaa", "aab", "aac", "aba", "abb", "abc", "aca", "acb", "acc", "baa", "bab", "bac", "bba", "bbb", "bbc", "bca", "bcb", "bcc", "caa", "cab", "cac", "cba", "cbb", "cbc", "cca", "ccb", "ccc"]

Key "gotchas" would be

  • *[arr]*a , which first creates an array of a arr s, then splats it into a arguments of product method.
  • map(&:join) , which is a shorthand for map{|e| e.join} map{|e| e.join}
  • inject (aka "reduce", from "map-reduce" fame), one of the FP pillars

In ruby, Array#product results in a Cathesian product. Adding the original array would yield the same results.

ar = (?a..?z).to_a
ar + ar.product(ar).map(&:join)

With the help of tokland, I've got it:

(1..2).each do |n|
  ('a'..'z').to_a.repeated_permutation(n).each do |a|
    s = a.join
    puts s
  end
end

And it's lazy, so it doesn't hog RAM when you're using it to generate longer strings.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM