简体   繁体   English

使用Itertools具有内部中断的可变级别的嵌套循环

[英]Variable levels of nested loops with inner breaks using Itertools

Experienced developer learning Python. 经验丰富的开发人员学习Python。

I'm iterating through the combinations k at a time from a list of size n. 我一次从大小为n的列表中遍历组合k。 I've been using 我一直在用

from itertools import chain, combinations
for subset in (combinations(range(n), k)) : 
    doSomethingWith(subset)

Now the problem is that most of the time my doSomethingWith()'s are not productive and I'd like to skip as many of them as I can. 现在的问题是,大多数时候,我的doSomethingWith()都不高效,我想尽可能多地跳过它们。 And if doSomthingWith() fails for a given subset, I can skip every subset whose rightmost coordinate is larger. 如果doSomthingWith()对于给定的子集失败,我可以跳过最右边坐标较大的每个子集。 In other words if it fails for (1,3,5,8) then the next subset I want to look at is (1,3,6,0), skipping (1,3,5,9), (1,3,5,10), etc. 换句话说,如果它失败了(1,3,5,8),那么我要查看的下一个子集是(1,3,6,0),跳过(1,3,5,9),(1, 3,5,10)等

I realized I need to get explicit control of the loop indices. 我意识到我需要对循环索引进行显式控制。 I need a variable number of nested for loops, using recursion or iteration. 我需要使用递归或迭代的可变数量的嵌套for循环。 Before coding that up I Googled around and found this thread which looks promising. 在进行编码之前,我在Google周围搜索,发现该线程看起来很有希望。

So now I have: 所以现在我有:

from itertools import product, repeat
set  = range(n)
sets = repeat(set, k)

for subset in list(product(*sets)) :
    doSomethingWith(subset)

Beautifully Pythonic but I still have the same problem. 漂亮的Pythonic,但我仍然有同样的问题。 I have no way to tell product() when to break out of the innermost loop. 我没有办法告诉product()何时脱离最内层的循环。 What I really want is to be able to pass a callback function into product() so it can execute and optionally break out of the innermost loop. 我真正想要的是能够将回调函数传递给product(),以便它可以执行并有选择地冲出最里面的循环。

Any Pythonic suggestions? 有Pythonic建议吗? I'd hate to have to explicitly code variable nested loops or iterate through the return from product() and examine the subsets by hand. 我讨厌必须显式地编写变量嵌套循环的代码,或者遍历product()的返回并手动检查子集。 That seems so old school :-) 似乎太老了:-)

This is an intriguing problem. 这是一个有趣的问题。 I concocted a generator that can be used to achieve something like what you want. 我设计了一个生成器,可以用来实现您想要的东西。 This is more of a prototype than a full solution. 这不仅仅是完整的解决方案,而是更多的原型。 It may need to be tweaked to be really useful, and there may be edge cases I haven't considered. 可能需要对其进行调整才能真正有用,并且可能存在一些我没有考虑过的情况。 (For one thing, right now it won't work properly on arbitrary iterables that might be exhaused, only on re-iterables like lists.) (一方面,目前它无法在可能用尽的任意可迭代对象上正常工作,仅在列表等可迭代对象上起作用。)

class SkipUp(Exception):
    def __init__(self, numSkips):
        self.numSkips = numSkips
        super(SkipUp, self).__init__(numSkips)

def breakableProduct(*sets):
    if not sets:
        yield []
        return
    first, rest = sets[0], sets[1:]
    for item in first:
        subProd = breakableProduct(*rest)
        for items in subProd:
            try:
                yield [item] + items
            except SkipUp as e:
                if e.numSkips == 0:
                    yield None
                    break
                else:
                    e.numSkips -= 1
                    yield subProd.throw(e)

You can use breakableProduct more or less like the normal itertools.product : 您可以使用breakableProduct或多或少像正常itertools.product

>>> prod = breakableProduct([1, 2, 3], [11, 22, 33], [111, 222, 333])
... for x, y, z in prod:
...     print(x, y, z)
1 11 111
1 11 222
1 11 333
1 22 111
1 22 222
# ...etc...
3 33 222
3 33 333

However, after getting a value from it, you can if you wish use .throw to pass in a SkipUp exception whose argument is the index of the set whose next element you want to skip to. 但是,从中获取值后,如果希望使用.throw可以传入SkipUp异常,该异常的参数是要跳过其下一个元素的集合的索引。 That is, if you want to skip over all elements of the third set and jump to the next element of the second set, use SkipUp(1) (1 is the second set because indexing is 0-based): 也就是说,如果您要跳过第三组的所有元素并跳到第二组的下一个元素,请使用SkipUp(1) (1是第二组,因为索引是基于0的):

>>> prod = breakableProduct([1, 2, 3], [11, 22, 33], [111, 222, 333])
... for x, y, z in prod:
...     print(x, y, z)
...     if z == 222:
...         prod.throw(SkipUp(1))
1 11 111
1 11 222
1 22 111
1 22 222
1 33 111
1 33 222
2 11 111
2 11 222
2 22 111
2 22 222
2 33 111
2 33 222
3 11 111
3 11 222
3 22 111
3 22 222
3 33 111
3 33 222

See how this aborts the innermost iteration after 222 , instead jumping forward to the next iteration of the middle generator. 看看这如何在222之后中止最内层的迭代,而不是跳转到中间生成器的下一个迭代。 If you want to jump out all the way to the outermost iteration: 如果要一直跳到最外层迭代:

>>> prod = breakableProduct([1, 2, 3], [11, 22, 33], [111, 222, 333])
... for x, y, z in prod:
...     print(x, y, z)
...     if z == 222:
...         prod.throw(SkipUp(0))
1 11 111
1 11 222
2 11 111
2 11 222
3 11 111
3 11 222

So SkipUp(0) jumps out to the next "tick" of the first iterator (ie, the list [1, 2, 3] ). 所以SkipUp(0)跳到第一个迭代器的下一个“滴答”(即列表[1, 2, 3] )。 Throwing in SkipUp(2) would have no effect, since it would just mean "skip to the next iteration of the innermost iterator", which is what the regular iteration would do anyway. 抛出SkipUp(2)不会有任何效果,因为这仅表示“跳到最里面的迭代器的下一个迭代”,这是常规迭代无论如何都会做的。

The advantage of this over a solution using something like product or combinations from itertools is that you can't stop those generators from generating every combination. 与使用itertools productcombinations类的解决方案相比,此方法的优势在于,您不能阻止这些生成器生成所有组合。 So even if you want to skip over some elements, itertools will still do all the looping to generate all the ones you don't want, and you'll only be discarding them after they're already generated. 因此,即使您想跳过某些元素,itertools仍将执行所有循环以生成所有不需要的元素,并且仅在它们生成后将其丢弃。 This breakableProduct , on the other hand, actually exits the inner loops early if you tell it to, so it will completely skip unnecessary iterations. breakableProduct ,在另一方面,实际上退出内环早期,如果你告诉它,所以它会完全跳过不必要的迭代。

Here's a fairly basic iterative ordered combination maker that takes a callback function. 这是一个相当基本的迭代有序组合器,它具有回调函数。 It's not as versatile as BrenBarn's solution, but it does skip generating products as specified in the question: if the callback fails when passed arg seq , returning False (or something false-ish), then combo will skip generating those other subsequences that commence with seq[:-1] . 它不像BrenBarn的解决方案那样通用,但是它确实跳过了问题中指定的生成乘积:如果在传递arg seq时回调失败,返回False (或假的东西),那么combo将跳过生成以以下开头的其他子序列seq[:-1]

def combo(n, k, callback):
    a = list(range(k))
    ok = callback(a)

    k -= 1
    while True:
        i = k
        if not ok: i -= 1
        while True:
            a[i] += 1
            if a[i] == (n + i - k):
                i -= 1
                if i < 0: 
                    return
            else:
                break 
        v = a[i] + 1  
        a[i+1:] = range(v, v + k - i) 
        ok = callback(a)

# test

n = 8
k = 4

def do_something(seq):
    s = sum(seq)
    ok = s <= seq[-2] * 3
    print(seq, s, ok)
    return ok

combo(n, k, do_something)

output 产量

[0, 1, 2, 3] 6 True
[0, 1, 2, 4] 7 False
[0, 1, 3, 4] 8 True
[0, 1, 3, 5] 9 True
[0, 1, 3, 6] 10 False
[0, 1, 4, 5] 10 True
[0, 1, 4, 6] 11 True
[0, 1, 4, 7] 12 True
[0, 1, 5, 6] 12 True
[0, 1, 5, 7] 13 True
[0, 1, 6, 7] 14 True
[0, 2, 3, 4] 9 True
[0, 2, 3, 5] 10 False
[0, 2, 4, 5] 11 True
[0, 2, 4, 6] 12 True
[0, 2, 4, 7] 13 False
[0, 2, 5, 6] 13 True
[0, 2, 5, 7] 14 True
[0, 2, 6, 7] 15 True
[0, 3, 4, 5] 12 True
[0, 3, 4, 6] 13 False
[0, 3, 5, 6] 14 True
[0, 3, 5, 7] 15 True
[0, 3, 6, 7] 16 True
[0, 4, 5, 6] 15 True
[0, 4, 5, 7] 16 False
[0, 4, 6, 7] 17 True
[0, 5, 6, 7] 18 True
[1, 2, 3, 4] 10 False
[1, 2, 4, 5] 12 True
[1, 2, 4, 6] 13 False
[1, 2, 5, 6] 14 True
[1, 2, 5, 7] 15 True
[1, 2, 6, 7] 16 True
[1, 3, 4, 5] 13 False
[1, 3, 5, 6] 15 True
[1, 3, 5, 7] 16 False
[1, 3, 6, 7] 17 True
[1, 4, 5, 6] 16 False
[1, 4, 6, 7] 18 True
[1, 5, 6, 7] 19 False
[2, 3, 4, 5] 14 False
[2, 3, 5, 6] 16 False
[2, 3, 6, 7] 18 True
[2, 4, 5, 6] 17 False
[2, 4, 6, 7] 19 False
[2, 5, 6, 7] 20 False
[3, 4, 5, 6] 18 False
[3, 4, 6, 7] 20 False
[3, 5, 6, 7] 21 False
[4, 5, 6, 7] 22 False

If you comment out the if not ok: i -= 1 line it will produce all combinations. 如果您将“ if not ok: i -= 1注释掉if not ok: i -= 1行,它将产生所有组合。


This code can be easily modified to do larger skips. 可以轻松修改此代码以进行更大的跳过。 Instead of using a boolean return from the callback we get it to return the skip level wanted. 与其从回调中使用布尔返回值,不如让我们返回想要的跳过级别。 If it returns zero then we don't do any skipping. 如果它返回零,那么我们不做任何跳过。 If it returns 1, then we skip the subsequences that commence with seq[:-1] as in the version above. 如果返回1,则与上述版本一样,我们跳过以seq[:-1]的子序列。 If the callback returns 2 then we skip the subsequences that commence with seq[:-2] , etc. 如果回调返回2,则我们跳过以seq[:-2]seq[:-2]的子序列。

def combo(n, k, callback):
    a = list(range(k))
    skips = callback(a)

    k -= 1
    while True:
        i = k - skips
        while True:
            a[i] += 1
            if a[i] == (n + i - k):
                i -= 1
                if i < 0: 
                    return
            else:
                break 
        v = a[i] + 1  
        a[i+1:] = range(v, v + k - i) 
        skips = callback(a)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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