简体   繁体   中英

How to build a dynamically growing nested list comprehension?

Assuming there is the following code which checks if the multiplied digits of a number equal an input number:

results = [a for a in range(10) if a == input]
results += [a*b for a in range(10) for b in range(10) if a*b == input]
results += [a*b*c for a in range(10) for b in range(10) for c in range(10) if a*b*c == input]
...

I want it to change so that it dynamically continues to search for a match if no result was found yet. So:

  • If one-digit numbers don't yield a result, continue with two-digit numbers
  • If two-digit numbers don't yield a result, continue with three-digit numbers
  • and so on ...

I'd like to do that in an elegant way, even a one-liner if that's not too involute. I also would need a break condition to avoid infinite loops if there is no match at all. For example if the input is a prime number > 10 there is no result. The break condition should be something like:

if(math.pow(2, countOfDigits) > input):
    return

where countOfDigits is the number of digits that are currently checked in the nested list comprehension. In other words, the first line of my initial example represents countOfDigits == 1 , the second line countOfDigits == 2 and the third line countOfDigits == 3 .

Oh, go on then:

next(
    sum(x) 
    for i in range(1, input) # overkill
    for x in itertools.product(range(10), repeat = i) 
    if reduce(operator.mul, x) == input
)

Edit: you've changed the question to return the product rather than the sum, so spit out input instead of sum(x) .

I'm not immediately sure whether you want the first match, or you want all matches where the number of factors is equal to the smallest possible. If the latter, you can do it by spitting input, i out of this iterator, then use itertools.groupby to collect them according to the second value in the tuple, then take only the first value in the result of that, and iterate over it to get all matches (although, since you're now outputting input that's kind of uninteresting other than maybe for its length).

Edit:

What you want is a thing which can be iterated over, but which does no work until as late as possible. That's not a list, and so a list comprehension is the wrong tool. Fortunately, you can use a generator expression . Your code is quite elaborate, so we'll want to probably use some helpers defined in the standard library, in itertools .

lets start by looking at the general case of your parts:

[n

   for x0 in range(10) 
   for x1 in range(10)
   ...
   for xn in range(10) 

 if x0 * x1 * ... * xn == input]

We have three parts to generalize. We'll start with the nested for loops, as arguments of N. for this we'll use itertools.product , It takes a sequence of sequences, something like [range(10), range(10), ... , range(10)] and produces every possible combination of items out of those sequences. In the special case of looping over a sequence multiple times, you can just pass the depth of nesting as repeat , so we can get to:

[n

   for x in itertools.product(xrange(10), repeat=n)

 if x[0] * x[1] * ... * x[n] == input]

For the sum in the output, we can flatten it to a single value with sum() , for the product, there's not really an equivalent. We can make one out of reduce and a function that multiplies two numbers together, which we can get from another standard library: operator.mul :

(n
 for x in itertools.product(xrange(10), repeat=n)
 if reduce(operator.mul, x, 1) == input)

So far so good, now we just need to repeat this inner part for every value of n. Supposing we want to search forever, we can get an unending sequence of numbers with itertools.count(1) , and Finally, we just need to turn this flatten sequence of sequences into a single sequence, for which we can use itertools.chain.from_iterable

itertools.chain.from_iterable(
     (n
      for x in itertools.product(xrange(10), repeat=n)
      if reduce(operator.mul, x, 1) == input)
     for n in itertools.count(1)
     if 2 ** n > input))
In [32]: input = 32

In [33]: next(itertools.chain.from_iterable(
        (n
         for x in itertools.product(xrange(10), repeat=n)
         if reduce(operator.mul, x, 1) == input)
        for n in itertools.count(1) if 2 ** n > input))
Out[33]: 6

I don't think you want a list comprehension. I think a generator would be better here:

def generate(x):
    for digits in itertools.count(1):
        for i in itertools.product(range(1, 10), repeat=digits):
            if reduce(operator.mul, i) == x:
                yield i
    if (math.pow(2, digits) > x):
        break

Then you do for i in generate(input_number) .

(You also need import itertools , from functools import reduce and import math at the top).

((math.pow(2, digits) > x) is just the break condition.)

You have several sequences, each of which may or may not contain the solution. Convert them to generators, use itertools to "chain" the sequences together, and then ask for the first element of the resulting sequence (notice that a sequence that does not contain the solution will be empty).

First of all, you need a way to generate sequences of n -tuples for any n . The itertools module has several functions that have effect comparable to nested for-loops. The one that matches your algorithm is itertools.product . The following generates all tuples of n digits:

tuples = itertools.product(range(10), repeat=n) 

It's actually better to use itertools.combinations_with_replacement , because there's no point testing both (4,5) and (5,4) . It has similar syntax. So here's a generator that will give you an infinite sequence of n-tuples, for increasing n :

sequences = ( itertools.product(range(10), repeat=n) for n in itertools.count(1) )

Next, you want to string the sequences together (without actually traversing them yet), into a single sequence. Because these are generators, they will only be evaluated if needed.

bigchain = itertools.chain.from_iterable(sequences)

bigchain will spit out all tuples you need to check. To test them, you need a way to multiply a tuple of arbitrary length. Let's define it:

def mytest(x):
    return reduce(operator.mul, x, 1) == target

You can now use this test to "filter" this sequence to select only tuples that match (there will always be many, since you include digit 1 in your combinations), then ask for the first one.

print itertools.islice(ifilter(mytest, bigchain), 1).next()     

I've changed your code to return your solution as a tuple, because otherwise, well, you'll just get back the number you were searching for (eg, 32 )-- which doesn't tell you anything you didn't know!

Here it is, all together:

from itertools import *
import operator

target = 32

sequences = ( combinations_with_replacement(range(10), n) for n in count(1) )
bigchain = chain.from_iterable(sequences)

def mytest(x):
    return reduce(operator.mul, x, 1) == target

print islice(ifilter(mytest, bigchain), 1).next()
# prints (4, 8)

You could also eliminate the intermediate variables in the above and combine everything into one expression; but what would be the point?

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